Skip to content

Commit

Permalink
feat(webhooks): validate Freight during creation (#2118)
Browse files Browse the repository at this point in the history
Signed-off-by: Hidde Beydals <[email protected]>
  • Loading branch information
hiddeco authored Jun 11, 2024
1 parent 9f93bec commit 9aa396b
Show file tree
Hide file tree
Showing 2 changed files with 573 additions and 7 deletions.
215 changes: 208 additions & 7 deletions internal/webhook/freight/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package freight
import (
"context"
"fmt"
"path"
"strings"

"github.com/technosophos/moniker"
Expand All @@ -11,13 +12,16 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

kargoapi "github.com/akuity/kargo/api/v1alpha1"
"github.com/akuity/kargo/internal/git"
"github.com/akuity/kargo/internal/helm"
"github.com/akuity/kargo/internal/kubeclient"
libEvent "github.com/akuity/kargo/internal/kubernetes/event"
libWebhook "github.com/akuity/kargo/internal/webhook"
Expand Down Expand Up @@ -65,6 +69,10 @@ type webhook struct {
...client.ListOption,
) error

getWarehouseFn func(context.Context, client.Client, types.NamespacedName) (*kargoapi.Warehouse, error)

validateFreightArtifactsFn func(*kargoapi.Freight, *kargoapi.Warehouse) error

isRequestFromKargoControlplaneFn libWebhook.IsRequestFromKargoControlplaneFn
}

Expand Down Expand Up @@ -100,8 +108,9 @@ func newWebhook(
w.validateProjectFn = libWebhook.ValidateProject
w.listFreightFn = kubeClient.List
w.listStagesFn = kubeClient.List
w.isRequestFromKargoControlplaneFn =
libWebhook.IsRequestFromKargoControlplane(cfg.ControlplaneUserRegex)
w.getWarehouseFn = kargoapi.GetWarehouse
w.validateFreightArtifactsFn = validateFreightArtifacts
w.isRequestFromKargoControlplaneFn = libWebhook.IsRequestFromKargoControlplane(cfg.ControlplaneUserRegex)
return w
}

Expand Down Expand Up @@ -146,8 +155,7 @@ func (w *webhook) ValidateCreate(
obj runtime.Object,
) (admission.Warnings, error) {
freight := obj.(*kargoapi.Freight) // nolint: forcetypeassert
if err :=
w.validateProjectFn(ctx, w.client, freightGroupKind, freight); err != nil {
if err := w.validateProjectFn(ctx, w.client, freightGroupKind, freight); err != nil {
return nil, err
}

Expand All @@ -172,9 +180,7 @@ func (w *webhook) ValidateCreate(
)
}

if len(freight.Commits) == 0 &&
len(freight.Images) == 0 &&
len(freight.Charts) == 0 {
if len(freight.Commits) == 0 && len(freight.Images) == 0 && len(freight.Charts) == 0 {
return nil, apierrors.NewInvalid(
freightGroupKind,
freight.Name,
Expand All @@ -187,6 +193,32 @@ func (w *webhook) ValidateCreate(
},
)
}

warehouse, err := w.getWarehouseFn(ctx, w.client, types.NamespacedName{
Namespace: freight.Namespace,
Name: freight.Warehouse,
})
if err != nil {
return nil, err
}
if warehouse == nil {
return nil, apierrors.NewInvalid(
freightGroupKind,
freight.Name,
field.ErrorList{
field.Invalid(
field.NewPath("warehouse"),
freight.Warehouse,
"warehouse does not exist",
),
},
)
}

if err := w.validateFreightArtifactsFn(freight, warehouse); err != nil {
return nil, err
}

return nil, nil
}

Expand Down Expand Up @@ -300,3 +332,172 @@ func (w *webhook) recordFreightApprovedEvent(
actor,
)
}

type artifactType string

func (a artifactType) FreightPath() string {
switch a {
case artifactTypeGit:
return "commits"
case artifactTypeImage:
return "images"
case artifactTypeChart:
return "charts"
default:
return ""
}
}

const (
artifactTypeGit artifactType = "git"
artifactTypeImage artifactType = "image"
artifactTypeChart artifactType = "chart"
)

type artifactSubscription struct {
URL string
Type artifactType
}

// validateFreightArtifacts checks that the artifacts in the Freight are all
// subscribed to by the Warehouse. It returns an error if:
//
// - An artifact in the Freight is not subscribed to by the Warehouse.
// - An artifact for a subscription of the Warehouse is not found in the Freight.
// - Multiple artifacts in the Freight correspond to the same subscription.
func validateFreightArtifacts(
freight *kargoapi.Freight,
warehouse *kargoapi.Warehouse,
) error {
var subscriptions = make(map[artifactSubscription]bool, len(warehouse.Spec.Subscriptions))
var counts = make(map[artifactSubscription]int)

// Collect all the subscriptions from the Warehouse.
for _, repo := range warehouse.Spec.Subscriptions {
if repo.Git != nil {
subscriptions[artifactSubscription{
URL: git.NormalizeURL(repo.Git.RepoURL),
Type: artifactTypeGit,
}] = false
}
if repo.Image != nil {
subscriptions[artifactSubscription{
URL: repo.Image.RepoURL,
Type: artifactTypeImage,
}] = false
}
if repo.Chart != nil {
subscriptions[artifactSubscription{
URL: path.Join(helm.NormalizeChartRepositoryURL(repo.Chart.RepoURL), repo.Chart.Name),
Type: artifactTypeChart,
}] = false
}
}

// Mark the subscription as found for each artifact in the Freight, and count
// the number of times each subscription is found.
for _, commit := range freight.Commits {
sub := artifactSubscription{
URL: git.NormalizeURL(commit.RepoURL),
Type: artifactTypeGit,
}
if _, ok := subscriptions[sub]; ok {
subscriptions[sub] = true
counts[sub]++
continue
}
return apierrors.NewInvalid(
freightGroupKind,
freight.Name,
field.ErrorList{
field.Invalid(
field.NewPath("commits"),
commit,
fmt.Sprintf("no subscription found for Git repository in Warehouse %q", warehouse.Name),
),
},
)
}
for _, image := range freight.Images {
sub := artifactSubscription{
URL: image.RepoURL,
Type: artifactTypeImage,
}
if _, ok := subscriptions[sub]; ok {
subscriptions[sub] = true
counts[sub]++
continue
}
return apierrors.NewInvalid(
freightGroupKind,
freight.Name,
field.ErrorList{
field.Invalid(
field.NewPath("images"),
image,
fmt.Sprintf("no subscription found for image repository in Warehouse %q", warehouse.Name),
),
},
)
}
for _, chart := range freight.Charts {
sub := artifactSubscription{
URL: path.Join(helm.NormalizeChartRepositoryURL(chart.RepoURL), chart.Name),
Type: artifactTypeChart,
}
if _, ok := subscriptions[sub]; ok {
subscriptions[sub] = true
counts[sub]++
continue
}
return apierrors.NewInvalid(
freightGroupKind,
freight.Name,
field.ErrorList{
field.Invalid(
field.NewPath("charts"),
chart,
fmt.Sprintf("no subscription found for Helm chart in Warehouse %q", warehouse.Name),
),
},
)
}

// Check that each subscription is found exactly once.
for sub, found := range subscriptions {
if !found {
return apierrors.NewInvalid(
freightGroupKind,
freight.Name,
field.ErrorList{
field.Invalid(
field.NewPath(sub.Type.FreightPath()),
nil,
fmt.Sprintf(
"no artifact found for subscription %q of Warehouse %q",
sub.URL, warehouse.Name,
),
),
},
)
}
if counts[sub] > 1 {
return apierrors.NewInvalid(
freightGroupKind,
freight.Name,
field.ErrorList{
field.Invalid(
field.NewPath(sub.Type.FreightPath()),
nil,
fmt.Sprintf(
"multiple artifacts found for subscription %q of Warehouse %q",
sub.URL, warehouse.Name,
),
),
},
)
}
}

return nil
}
Loading

0 comments on commit 9aa396b

Please sign in to comment.