From dc5408f475089f8fc1f755aa5db43894239f06a0 Mon Sep 17 00:00:00 2001 From: Ville Aikas Date: Tue, 8 Mar 2022 15:19:12 +0200 Subject: [PATCH] This is the start of the necessary pieces to get #1418 and #1419 implemented ClusterImagePolicy reconciler will now create a configmap (no secret support yet) and update it on changes (not on deletions yet). Also put up most necessary testing pieces so that we can start unit testing the reconciler and make sure it updates the resulting configmap. There's also a ConfigStore that we can then inject into the admission webhook that I have wired in there (nop for now, but demonstrating how it could work). Idea being that you could then for a given image ask for all the authorities that need to be validated. You can see what that config looks like in the /pkg/apis/config/testdata/image-policies.yaml and the accompanying tests in /pkg/apis/config/image_policies_test I made sure that it works with both yaml/json. While playing with this there's some questions that came to mind, so I'll take those to the document. Hope is that we get enough pieces in place so that we can agree on the major moving pieces and how they fit together and enough testing in place that we can start sharding up the work more efficiently and in more focused areas. Signed-off-by: Ville Aikas --- cmd/cosign/webhook/main.go | 11 + go.mod | 2 +- pkg/apis/config/doc.go | 24 ++ pkg/apis/config/image_policies.go | 105 +++++++++ pkg/apis/config/image_policies_test.go | 111 +++++++++ pkg/apis/config/store.go | 91 ++++++++ pkg/apis/config/store_test.go | 63 ++++++ pkg/apis/config/testdata/image-policies.yaml | 67 ++++++ pkg/cosign/kubernetes/webhook/validation.go | 15 ++ .../clusterimagepolicy/clusterimagepolicy.go | 69 +++++- .../clusterimagepolicy_test.go | 214 ++++++++++++++++++ .../clusterimagepolicy/controller.go | 36 ++- .../clusterimagepolicy/controller_test.go | 37 +++ .../clusterimagepolicy/resources/configmap.go | 74 ++++++ .../testing/v1alpha1/clusterimagepolicy.go | 51 +++++ pkg/reconciler/testing/v1alpha1/factory.go | 153 +++++++++++++ pkg/reconciler/testing/v1alpha1/listers.go | 91 ++++++++ third_party/VENDOR-LICENSE/LICENSE | 27 +++ .../github.com/benbjohnson/clock/LICENSE | 21 ++ .../vendor/golang.org/x/crypto/LICENSE | 27 +++ .../vendor/golang.org/x/net/LICENSE | 27 +++ .../vendor/golang.org/x/text/LICENSE | 27 +++ 22 files changed, 1338 insertions(+), 5 deletions(-) create mode 100644 pkg/apis/config/doc.go create mode 100644 pkg/apis/config/image_policies.go create mode 100644 pkg/apis/config/image_policies_test.go create mode 100644 pkg/apis/config/store.go create mode 100644 pkg/apis/config/store_test.go create mode 100644 pkg/apis/config/testdata/image-policies.yaml create mode 100644 pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go create mode 100644 pkg/reconciler/clusterimagepolicy/controller_test.go create mode 100644 pkg/reconciler/clusterimagepolicy/resources/configmap.go create mode 100644 pkg/reconciler/testing/v1alpha1/clusterimagepolicy.go create mode 100644 pkg/reconciler/testing/v1alpha1/factory.go create mode 100644 pkg/reconciler/testing/v1alpha1/listers.go create mode 100644 third_party/VENDOR-LICENSE/LICENSE create mode 100644 third_party/VENDOR-LICENSE/github.com/benbjohnson/clock/LICENSE create mode 100644 third_party/VENDOR-LICENSE/vendor/golang.org/x/crypto/LICENSE create mode 100644 third_party/VENDOR-LICENSE/vendor/golang.org/x/net/LICENSE create mode 100644 third_party/VENDOR-LICENSE/vendor/golang.org/x/text/LICENSE diff --git a/cmd/cosign/webhook/main.go b/cmd/cosign/webhook/main.go index 988e41704e95..cbedc55c85fb 100644 --- a/cmd/cosign/webhook/main.go +++ b/cmd/cosign/webhook/main.go @@ -29,6 +29,7 @@ import ( "knative.dev/pkg/configmap" "knative.dev/pkg/controller" "knative.dev/pkg/injection/sharedmain" + "knative.dev/pkg/logging" "knative.dev/pkg/signals" "knative.dev/pkg/webhook" "knative.dev/pkg/webhook/certificates" @@ -37,6 +38,7 @@ import ( "knative.dev/pkg/webhook/resourcesemantics/validation" "sigs.k8s.io/release-utils/version" + "github.com/sigstore/cosign/pkg/apis/config" cwebhook "github.com/sigstore/cosign/pkg/cosign/kubernetes/webhook" "github.com/sigstore/cosign/pkg/reconciler/clusterimagepolicy" ) @@ -85,6 +87,9 @@ var types = map[schema.GroupVersionKind]resourcesemantics.GenericCRD{ } func NewValidatingAdmissionController(ctx context.Context, cmw configmap.Watcher) *controller.Impl { + // Decorate contexts with the current state of the config. + store := config.NewStore(logging.FromContext(ctx).Named("config-store")) + store.WatchConfigs(cmw) validator := cwebhook.NewValidator(ctx, *secretName) return validation.NewAdmissionController(ctx, @@ -99,6 +104,7 @@ func NewValidatingAdmissionController(ctx context.Context, cmw configmap.Watcher // A function that infuses the context passed to Validate/SetDefaults with custom metadata. func(ctx context.Context) context.Context { + ctx = store.ToContext(ctx) ctx = duckv1.WithPodValidator(ctx, validator.ValidatePod) ctx = duckv1.WithPodSpecValidator(ctx, validator.ValidatePodSpecable) ctx = duckv1.WithCronJobValidator(ctx, validator.ValidateCronJob) @@ -115,6 +121,10 @@ func NewValidatingAdmissionController(ctx context.Context, cmw configmap.Watcher } func NewMutatingAdmissionController(ctx context.Context, cmw configmap.Watcher) *controller.Impl { + // Decorate contexts with the current state of the config. + store := config.NewStore(logging.FromContext(ctx).Named("config-store")) + store.WatchConfigs(cmw) + validator := cwebhook.NewValidator(ctx, *secretName) return defaulting.NewAdmissionController(ctx, @@ -129,6 +139,7 @@ func NewMutatingAdmissionController(ctx context.Context, cmw configmap.Watcher) // A function that infuses the context passed to Validate/SetDefaults with custom metadata. func(ctx context.Context) context.Context { + ctx = store.ToContext(ctx) ctx = duckv1.WithPodDefaulter(ctx, validator.ResolvePod) ctx = duckv1.WithPodSpecDefaulter(ctx, validator.ResolvePodSpecable) ctx = duckv1.WithCronJobDefaulter(ctx, validator.ResolveCronJob) diff --git a/go.mod b/go.mod index 26eb6cc3440a..570e2254e0b8 100644 --- a/go.mod +++ b/go.mod @@ -56,6 +56,7 @@ require ( k8s.io/utils v0.0.0-20220127004650-9b3446523e65 knative.dev/pkg v0.0.0-20220202132633-df430fa0dd96 sigs.k8s.io/release-utils v0.4.1-0.20220207182343-6dadf2228617 + sigs.k8s.io/yaml v1.3.0 ) require ( @@ -264,7 +265,6 @@ require ( k8s.io/gengo v0.0.0-20211129171323-c02415ce4185 // indirect k8s.io/klog/v2 v2.40.1 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect ) require ( diff --git a/pkg/apis/config/doc.go b/pkg/apis/config/doc.go new file mode 100644 index 000000000000..efb3aa93aa94 --- /dev/null +++ b/pkg/apis/config/doc.go @@ -0,0 +1,24 @@ +// +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +k8s:deepcopy-gen=package + +// Package config holds the typed objects that define the schemas for +// ConfigMap objects that pertain to our API objects. +// This ConfigMap gets created by the Reconciler by combining all the +// ClusterImagePolicy CR into a single ConfigMap so that the AdmissionController +// only needs to deal with a single resource when validationg. + +package config diff --git a/pkg/apis/config/image_policies.go b/pkg/apis/config/image_policies.go new file mode 100644 index 000000000000..d34005b28e62 --- /dev/null +++ b/pkg/apis/config/image_policies.go @@ -0,0 +1,105 @@ +// +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" +) + +const ( + // ImagePoliciesConfigName is the name of ConfigMap created by the + // reconciler and consumed by the admission webhook. + ImagePoliciesConfigName = "image-policies" +) + +type ImagePolicyConfig struct { + // This is the list of ImagePolicies that a admission controller uses + // to make policy decisions. + // TODO(vaikas): Revisit the datastructure. For now seems fine to use the + // same definitions from the API definition. The _only_ difference is that + // we do not use SecretReference fields, they get resolved and inlined + // in the Secret. So it seemed unnecessary to mirror those all over. + // Key in the map is the name of the ClusterImagePolicy where the Policy + // was received from. + Policies map[string]v1alpha1.ClusterImagePolicySpec +} + +// NewImagePoliciesConfigFromMap creates an ImagePolicyConfig from the supplied +// Map +func NewImagePoliciesConfigFromMap(data map[string]string) (*ImagePolicyConfig, error) { + ret := &ImagePolicyConfig{Policies: make(map[string]v1alpha1.ClusterImagePolicySpec, len(data))} + // Spin through the ConfigMap. Each key will point to resolved + // ImagePatterns. + for k, v := range data { + // This is the example that we use to document / test the ConfigMap. + if k == "_example" { + continue + } + if v == "" { + return nil, fmt.Errorf("ConfigMap has an entry %q but no value", k) + } + clusterImagePolicy := &v1alpha1.ClusterImagePolicySpec{} + + if err := parseEntry(v, clusterImagePolicy); err != nil { + return nil, fmt.Errorf("Failed to parse the entry %q : %q : %s", k, v, err) + } + fmt.Printf("GOT CIP: %+v\n", clusterImagePolicy) + ret.Policies[k] = *clusterImagePolicy + } + return ret, nil +} + +// NewImagePoliciesConfigFromConfigMap creates a Features from the supplied ConfigMap +func NewImagePoliciesConfigFromConfigMap(config *corev1.ConfigMap) (*ImagePolicyConfig, error) { + return NewImagePoliciesConfigFromMap(config.Data) +} + +func parseEntry(entry string, out interface{}) error { + j, err := yaml.YAMLToJSON([]byte(entry)) + if err != nil { + return fmt.Errorf("ConfigMap's value could not be converted to JSON: %s : %v", err, entry) + } + return json.Unmarshal(j, &out) +} + +// GetAuthorities returns all matching Authorities that need to be matched for +// the given Image. +func (p *ImagePolicyConfig) GetAuthorities(image string) ([]v1alpha1.Authority, error) { + if p == nil { + return nil, errors.New("Config is nil") + } + + ret := []v1alpha1.Authority{} + + // TODO(vaikas): this is very inefficient, we should have a better + // way to go from image to Authorities, but just seeing if this is even + // workable so fine for now. + for _, v := range p.Policies { + for _, pattern := range v.Images { + // TODO(vaikas): do the actual glob match. + if image == pattern.Glob { + ret = append(ret, pattern.Authorities...) + } + } + } + return ret, nil +} diff --git a/pkg/apis/config/image_policies_test.go b/pkg/apis/config/image_policies_test.go new file mode 100644 index 000000000000..5efb02a918d0 --- /dev/null +++ b/pkg/apis/config/image_policies_test.go @@ -0,0 +1,111 @@ +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "testing" + + . "knative.dev/pkg/configmap/testing" + _ "knative.dev/pkg/system/testing" +) + +const ( + // Just some public key that was laying around, only format matters. + inlineKeyData = `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J +RCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ== +-----END PUBLIC KEY-----` +) + +func TestDefaultsConfigurationFromFile(t *testing.T) { + _, example := ConfigMapsFromTestFile(t, ImagePoliciesConfigName) + if _, err := NewImagePoliciesConfigFromConfigMap(example); err != nil { + t.Error("NewImagePoliciesConfigFromConfigMap(example) =", err) + } +} + +func TestGetAuthorities(t *testing.T) { + _, example := ConfigMapsFromTestFile(t, ImagePoliciesConfigName) + defaults, err := NewImagePoliciesConfigFromConfigMap(example) + if err != nil { + t.Error("NewImagePoliciesConfigFromConfigMap(example) =", err) + } + c, err := defaults.GetAuthorities("rando") + if err != nil { + t.Error("GetMatches Failed =", err) + } + if len(c) == 0 { + t.Error("Wanted a config, got none.") + } + want := "inlinedata here" + if got := c[0].Key.Data; got != want { + t.Errorf("Did not get what I wanted %q, got %+v", want, c[0].Key.Data) + } + c, err = defaults.GetAuthorities("rando*") + if err != nil { + t.Error("GetMatches Failed =", err) + } + if len(c) == 0 { + t.Error("Wanted a config, got none.") + } + want = "otherinline here" + if got := c[0].Key.Data; got != want { + t.Errorf("Did not get what I wanted %q, got %+v", want, c[0].Key.Data) + } + c, err = defaults.GetAuthorities("rando3") + if err != nil { + t.Error("GetMatches Failed =", err) + } + if len(c) == 0 { + t.Error("Wanted a config, got none.") + } + want = "cakey chilling here" + if got := c[0].Keyless.CAKey.Data; got != want { + t.Errorf("Did not get what I wanted %q, got %+v", want, c[0].Keyless.CAKey.Data) + } + want = "issuer" + if got := c[0].Keyless.Identities[0].Issuer; got != want { + t.Errorf("Did not get what I wanted %q, got %+v", want, c[0].Keyless.Identities[0].Issuer) + } + want = "subject" + if got := c[0].Keyless.Identities[0].Subject; got != want { + t.Errorf("Did not get what I wanted %q, got %+v", want, c[0].Keyless.Identities[0].Subject) + } + // Test multiline yaml cert + c, err = defaults.GetAuthorities("inlinecert") + if err != nil { + t.Error("GetMatches Failed =", err) + } + if len(c) == 0 { + t.Error("Wanted a config, got none.") + } + want = inlineKeyData + if got := c[0].Key.Data; got != want { + t.Errorf("Did not get what I wanted %q, got %+v", want, c[0].Key.Data) + } + // Test multiline cert but json encoded + c, err = defaults.GetAuthorities("ghcr.io/example/*") + if err != nil { + t.Error("GetMatches Failed =", err) + } + if len(c) == 0 { + t.Error("Wanted a config, got none.") + } + want = inlineKeyData + if got := c[0].Key.Data; got != want { + t.Errorf("Did not get what I wanted %q, got %+v", want, c[0].Key.Data) + } + +} diff --git a/pkg/apis/config/store.go b/pkg/apis/config/store.go new file mode 100644 index 000000000000..1d5ec366ac19 --- /dev/null +++ b/pkg/apis/config/store.go @@ -0,0 +1,91 @@ +// +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "context" + + "knative.dev/pkg/configmap" +) + +type cfgKey struct{} + +// Config holds the collection of configurations that we attach to contexts. +// +k8s:deepcopy-gen=false +type Config struct { + ImagePolicyConfig *ImagePolicyConfig +} + +// FromContext extracts a Config from the provided context. +func FromContext(ctx context.Context) *Config { + x, ok := ctx.Value(cfgKey{}).(*Config) + if ok { + return x + } + return nil +} + +// FromContextOrDefaults is like FromContext, but when no Config is attached it +// returns a Config populated with the defaults for each of the Config fields. +func FromContextOrDefaults(ctx context.Context) *Config { + if cfg := FromContext(ctx); cfg != nil { + return cfg + } + config, _ := NewImagePoliciesConfigFromMap(map[string]string{}) + return &Config{ + ImagePolicyConfig: config, + } +} + +// ToContext attaches the provided Config to the provided context, returning the +// new context with the Config attached. +func ToContext(ctx context.Context, c *Config) context.Context { + return context.WithValue(ctx, cfgKey{}, c) +} + +// Store is a typed wrapper around configmap.Untyped store to handle our configmaps. +// +k8s:deepcopy-gen=false +type Store struct { + *configmap.UntypedStore +} + +// NewStore creates a new store of Configs and optionally calls functions when ConfigMaps are updated. +func NewStore(logger configmap.Logger, onAfterStore ...func(name string, value interface{})) *Store { + store := &Store{ + UntypedStore: configmap.NewUntypedStore( + "image-policies", + logger, + configmap.Constructors{ + ImagePoliciesConfigName: NewImagePoliciesConfigFromConfigMap, + }, + onAfterStore..., + ), + } + + return store +} + +// ToContext attaches the current Config state to the provided context. +func (s *Store) ToContext(ctx context.Context) context.Context { + return ToContext(ctx, s.Load()) +} + +// Load creates a Config from the current config state of the Store. +func (s *Store) Load() *Config { + return &Config{ + ImagePolicyConfig: s.UntypedLoad(ImagePoliciesConfigName).(*ImagePolicyConfig), + } +} diff --git a/pkg/apis/config/store_test.go b/pkg/apis/config/store_test.go new file mode 100644 index 000000000000..9045aee2b3c5 --- /dev/null +++ b/pkg/apis/config/store_test.go @@ -0,0 +1,63 @@ +/* +Copyright 2020 The Knative Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "k8s.io/apimachinery/pkg/api/resource" + logtesting "knative.dev/pkg/logging/testing" + + . "knative.dev/pkg/configmap/testing" +) + +var ignoreStuff = cmp.Options{ + cmpopts.IgnoreUnexported(resource.Quantity{}), +} + +func TestStoreLoadWithContext(t *testing.T) { + store := NewStore(logtesting.TestLogger(t)) + + _, imagePolicies := ConfigMapsFromTestFile(t, ImagePoliciesConfigName) + + store.OnConfigChanged(imagePolicies) + + config := FromContextOrDefaults(store.ToContext(context.Background())) + + t.Run("image-policies", func(t *testing.T) { + expected, _ := NewImagePoliciesConfigFromConfigMap(imagePolicies) + if diff := cmp.Diff(expected, config.ImagePolicyConfig, ignoreStuff...); diff != "" { + t.Error("Unexpected defaults config (-want, +got):", diff) + t.Fatal("Unexpected defaults config (-want, +got):", diff) + } + }) +} + +func TestStoreLoadWithContextOrDefaults(t *testing.T) { + imagePolicies := ConfigMapFromTestFile(t, ImagePoliciesConfigName) + config := FromContextOrDefaults(context.Background()) + + t.Run("image-policies", func(t *testing.T) { + expected, _ := NewImagePoliciesConfigFromConfigMap(imagePolicies) + if diff := cmp.Diff(expected, config.ImagePolicyConfig, ignoreStuff...); diff != "" { + t.Error("Unexpected defaults config (-want, +got):", diff) + } + }) +} diff --git a/pkg/apis/config/testdata/image-policies.yaml b/pkg/apis/config/testdata/image-policies.yaml new file mode 100644 index 000000000000..1a5d535bb834 --- /dev/null +++ b/pkg/apis/config/testdata/image-policies.yaml @@ -0,0 +1,67 @@ +# Copyright 2022 The Sigstore Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: image-policies + namespace: cosign-system + labels: + cosigned.sigstore.dev/release: devel + +data: + _example: | + ################################ + # # + # EXAMPLE CONFIGURATION # + # # + ################################ + cluster-image-policy-0: | + images: + - glob: rando + authorities: + - key: + data: inlinedata here + - key: + kms: whatevs + regex: stuff + cluster-image-policy-1: | + images: + - glob: rando* + authorities: + - key: + data: otherinline here + cluster-image-policy-2: | + images: + - glob: rando3 + authorities: + - keyless: + ca-key: + data: cakey chilling here + url: http://keylessurl.here + identities: + - issuer: issuer + subject: subject + cluster-image-policy-3: | + images: + - glob: inlinecert + authorities: + - key: + data: |- + -----BEGIN PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J + RCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ== + -----END PUBLIC KEY----- + cluster-image-policy-json: "{\"images\":[{\"glob\":\"ghcr.io/example/*\",\"regex\":\"\",\"authorities\":[{\"key\":{\"data\":\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\\n-----END PUBLIC KEY-----\"}}]}]}" + diff --git a/pkg/cosign/kubernetes/webhook/validation.go b/pkg/cosign/kubernetes/webhook/validation.go index dc0e0f718ec0..f6bd397ccb9d 100644 --- a/pkg/cosign/kubernetes/webhook/validation.go +++ b/pkg/cosign/kubernetes/webhook/validation.go @@ -29,6 +29,7 @@ import ( "knative.dev/pkg/logging" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioroots" + "github.com/sigstore/cosign/pkg/apis/config" "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/cosign/pkg/oci" ociremote "github.com/sigstore/cosign/pkg/oci/remote" @@ -36,6 +37,20 @@ import ( ) func valid(ctx context.Context, ref name.Reference, keys []*ecdsa.PublicKey, opts ...ociremote.Option) error { + // TODO(vaikas): No failures, just logging as to not interfere with the + // normal operation. Just starting to plumb things through here. + config := config.FromContext(ctx) + if config != nil { + authorities, err := config.ImagePolicyConfig.GetAuthorities(ref.Name()) + if err != nil { + logging.FromContext(ctx).Errorf("Failed to fetch authorities for %s : %v", ref.Name(), err) + } else { + for _, authority := range authorities { + logging.FromContext(ctx).Infof("TODO: Check authority for image: %s : Authority: %+v ", ref.Name(), authority) + } + } + } + if len(keys) == 0 { // If there are no keys, then verify against the fulcio root. sps, err := validSignatures(ctx, ref, nil /* verifier */, opts...) diff --git a/pkg/reconciler/clusterimagepolicy/clusterimagepolicy.go b/pkg/reconciler/clusterimagepolicy/clusterimagepolicy.go index e4f8d9e6d69a..28df4da1711b 100644 --- a/pkg/reconciler/clusterimagepolicy/clusterimagepolicy.go +++ b/pkg/reconciler/clusterimagepolicy/clusterimagepolicy.go @@ -16,9 +16,19 @@ package clusterimagepolicy import ( "context" + "fmt" + + "github.com/sigstore/cosign/pkg/apis/config" + "k8s.io/apimachinery/pkg/types" "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" clusterimagepolicyreconciler "github.com/sigstore/cosign/pkg/client/injection/reconciler/cosigned/v1alpha1/clusterimagepolicy" + "github.com/sigstore/cosign/pkg/reconciler/clusterimagepolicy/resources" + apierrs "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + corev1listers "k8s.io/client-go/listers/core/v1" + "knative.dev/pkg/logging" "knative.dev/pkg/reconciler" "knative.dev/pkg/tracker" ) @@ -28,13 +38,68 @@ import ( type Reconciler struct { // Tracker builds an index of what resources are watching other resources // so that we can immediately react to changes tracked resources. - Tracker tracker.Interface + tracker tracker.Interface + // We need to be able to read Secrets, which are really holding public + // keys. + secretlister corev1listers.SecretLister + configmaplister corev1listers.ConfigMapLister + kubeclient kubernetes.Interface } // Check that our Reconciler implements Interface var _ clusterimagepolicyreconciler.Interface = (*Reconciler)(nil) // ReconcileKind implements Interface.ReconcileKind. -func (r *Reconciler) ReconcileKind(ctx context.Context, o *v1alpha1.ClusterImagePolicy) reconciler.Event { +func (r *Reconciler) ReconcileKind(ctx context.Context, cip *v1alpha1.ClusterImagePolicy) reconciler.Event { + + if !willItBlend(cip) { + return fmt.Errorf("I can't do that yet, only support keys inlined or KMS") + } + + // See if the CM holding configs exists + existing, err := r.configmaplister.ConfigMaps(SystemNamespace).Get(config.ImagePoliciesConfigName) + if err != nil { + if !apierrs.IsNotFound(err) { + logging.FromContext(ctx).Errorf("Failed to get configmap: %v", err) + return err + } + // Does not exist, create it. + cm, err := resources.NewConfigMap(SystemNamespace, config.ImagePoliciesConfigName, cip) + if err != nil { + logging.FromContext(ctx).Errorf("Failed to construct configmap: %v", err) + return err + } + _, err = r.kubeclient.CoreV1().ConfigMaps(SystemNamespace).Create(ctx, cm, metav1.CreateOptions{}) + return err + } + + // Check if we need to update the configmap or not. + patchBytes, err := resources.CreatePatch(SystemNamespace, config.ImagePoliciesConfigName, existing.DeepCopy(), cip) + if err != nil { + logging.FromContext(ctx).Errorf("Failed to create patch: %v", err) + return err + } + if len(patchBytes) > 0 { + _, err = r.kubeclient.CoreV1().ConfigMaps(SystemNamespace).Patch(ctx, config.ImagePoliciesConfigName, types.JSONPatchType, patchBytes, metav1.PatchOptions{}) + return err + } return nil } + +// Checks to see if we can deal with format yet. This is missing support +// for things like Secret resolution, so we can't do those yet. As more things +// are supported, remove them from here. +func willItBlend(cip *v1alpha1.ClusterImagePolicy) bool { + for _, image := range cip.Spec.Images { + for _, authority := range image.Authorities { + if authority.Key != nil && authority.Key.SecretRef != nil { + return false + } + if authority.Keyless != nil && authority.Keyless.CAKey != nil && + authority.Keyless.CAKey.SecretRef != nil { + return false + } + } + } + return true +} diff --git a/pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go b/pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go new file mode 100644 index 000000000000..1c3c320573e7 --- /dev/null +++ b/pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go @@ -0,0 +1,214 @@ +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package clusterimagepolicy + +import ( + "context" + "testing" + + "github.com/sigstore/cosign/pkg/apis/config" + "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" + fakecosignclient "github.com/sigstore/cosign/pkg/client/injection/client/fake" + "github.com/sigstore/cosign/pkg/client/injection/reconciler/cosigned/v1alpha1/clusterimagepolicy" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgotesting "k8s.io/client-go/testing" + fakekubeclient "knative.dev/pkg/client/injection/kube/client/fake" + "knative.dev/pkg/configmap" + "knative.dev/pkg/controller" + logtesting "knative.dev/pkg/logging/testing" + + . "github.com/sigstore/cosign/pkg/reconciler/testing/v1alpha1" + . "knative.dev/pkg/reconciler/testing" +) + +const ( + testNS = "test-namespace" + cipName = "test-cip" + testKey = "test-cip" + cipName2 = "test-cip-2" + testKey2 = "test-cip-2" + glob = "ghcr.io/example/*" + kms = "azure-kms://foo/bar" + + // Just some public key that was laying around, only format matters. + inlineKeyData = `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J +RCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ== +-----END PUBLIC KEY-----` + // This is the patch for replacing a single entry in the ConfigMap + replaceCIPPatch = `[{"op":"replace","path":"/data/test-cip","value":"{\"images\":[{\"glob\":\"ghcr.io/example/*\",\"regex\":\"\",\"authorities\":[{\"key\":{\"data\":\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\\n-----END PUBLIC KEY-----\"}}]}]}"}]` + // This is the patch for adding an entry for non-existing KMS for cipName2 + addCIP2Patch = `[{"op":"add","path":"/data/test-cip-2","value":"{\"images\":[{\"glob\":\"ghcr.io/example/*\",\"regex\":\"\",\"authorities\":[{\"key\":{\"data\":\"azure-kms://foo/bar\"}}]}]}"}]` +) + +func TestReconcile(t *testing.T) { + table := TableTest{ + { + Name: "bad workqueue key", + // Make sure Reconcile handles bad keys. + Key: "too/many/parts", + }, { + Name: "key not found", + // Make sure Reconcile handles good keys that don't exist. + Key: "foo/not-found", + }, { + Name: "ClusterImagePolicy not found", + Key: testKey, + }, { + Name: "ClusterImagePolicy is being deleted", + Key: testKey, + Objects: []runtime.Object{ + NewClusterImagePolicy(cipName, + WithClusterImagePolicyDeletionTimestamp), + }, + }, { + Name: "ClusterImagePolicy with glob and inline key data", + Key: testKey, + + SkipNamespaceValidation: true, // Cluster scoped + Objects: []runtime.Object{ + NewClusterImagePolicy(cipName, + WithImagePattern(v1alpha1.ImagePattern{ + Glob: glob, + Authorities: []v1alpha1.Authority{ + { + Key: &v1alpha1.KeyRef{ + Data: inlineKeyData, + }, + }, + }, + })), + }, + WantCreates: []runtime.Object{ + makeConfigMap(cipName), + }, + }, { + Name: "ClusterImagePolicy with glob and inline key data, already exists, no patch", + Key: testKey, + + SkipNamespaceValidation: true, // Cluster scoped + Objects: []runtime.Object{ + NewClusterImagePolicy(cipName, + WithImagePattern(v1alpha1.ImagePattern{ + Glob: glob, + Authorities: []v1alpha1.Authority{ + { + Key: &v1alpha1.KeyRef{ + Data: inlineKeyData, + }, + }, + }, + })), + makeConfigMap(cipName), + }, + }, { + Name: "ClusterImagePolicy with glob and inline key data, needs a patch", + Key: testKey, + + SkipNamespaceValidation: true, // Cluster scoped + Objects: []runtime.Object{ + NewClusterImagePolicy(cipName, + WithImagePattern(v1alpha1.ImagePattern{ + Glob: glob, + Authorities: []v1alpha1.Authority{ + { + Key: &v1alpha1.KeyRef{ + Data: inlineKeyData, + }, + }, + }, + })), + makeDifferentConfigMap(cipName), + }, + WantPatches: []clientgotesting.PatchActionImpl{ + makePatch(SystemNamespace, config.ImagePoliciesConfigName, replaceCIPPatch), + }, + }, { + Name: "ClusterImagePolicy with glob and KMS key data, added as a patch", + Key: testKey2, + + SkipNamespaceValidation: true, // Cluster scoped + Objects: []runtime.Object{ + NewClusterImagePolicy(cipName2, + WithImagePattern(v1alpha1.ImagePattern{ + Glob: glob, + Authorities: []v1alpha1.Authority{ + { + Key: &v1alpha1.KeyRef{ + Data: kms, + }, + }, + }, + })), + makeConfigMap(cipName), // Make the existing configmap + }, + WantPatches: []clientgotesting.PatchActionImpl{ + makePatch(SystemNamespace, config.ImagePoliciesConfigName, addCIP2Patch), + }, + }, {}} + + logger := logtesting.TestLogger(t) + table.Test(t, MakeFactory(func(ctx context.Context, listers *Listers, cmw configmap.Watcher) controller.Reconciler { + r := &Reconciler{ + secretlister: listers.GetSecretLister(), + configmaplister: listers.GetConfigMapLister(), + kubeclient: fakekubeclient.Get(ctx), + } + return clusterimagepolicy.NewReconciler(ctx, logger, + fakecosignclient.Get(ctx), listers.GetClusterImagePolicyLister(), + controller.GetEventRecorder(ctx), + r) + }, + false, + logger, + )) +} + +func makeConfigMap(cipName string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: SystemNamespace, + Name: config.ImagePoliciesConfigName, + }, + Data: map[string]string{ + cipName: `{"images":[{"glob":"ghcr.io/example/*","regex":"","authorities":[{"key":{"data":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\n-----END PUBLIC KEY-----"}}]}]}`, + }, + } +} + +// Same as above, just forcing an update by changing PUBLIC => NOTPUBLIC +func makeDifferentConfigMap(cipName string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: SystemNamespace, + Name: config.ImagePoliciesConfigName, + }, + Data: map[string]string{ + cipName: `{"images":[{"glob":"ghcr.io/example/*","regex":"","authorities":[{"key":{"data":"-----BEGIN NOTPUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExB6+H6054/W1SJgs5JR6AJr6J35J\nRCTfQ5s1kD+hGMSE1rH7s46hmXEeyhnlRnaGF8eMU/SBJE/2NKPnxE7WzQ==\n-----END NOTPUBLIC KEY-----"}}]}]}`, + }, + } +} + +func makePatch(namespace, name, patch string) clientgotesting.PatchActionImpl { + return clientgotesting.PatchActionImpl{ + ActionImpl: clientgotesting.ActionImpl{ + Namespace: namespace, + }, + Name: name, + Patch: []byte(patch), + } +} diff --git a/pkg/reconciler/clusterimagepolicy/controller.go b/pkg/reconciler/clusterimagepolicy/controller.go index 05f53b35f606..fadc0ba75886 100644 --- a/pkg/reconciler/clusterimagepolicy/controller.go +++ b/pkg/reconciler/clusterimagepolicy/controller.go @@ -17,25 +17,57 @@ package clusterimagepolicy import ( "context" + "k8s.io/client-go/tools/cache" + kubeclient "knative.dev/pkg/client/injection/kube/client" + configmapinformer "knative.dev/pkg/client/injection/kube/informers/core/v1/configmap" + secretinformer "knative.dev/pkg/client/injection/kube/informers/core/v1/secret" "knative.dev/pkg/configmap" "knative.dev/pkg/controller" + "knative.dev/pkg/logging" + pkgreconciler "knative.dev/pkg/reconciler" + "github.com/sigstore/cosign/pkg/apis/config" clusterimagepolicyinformer "github.com/sigstore/cosign/pkg/client/injection/informers/cosigned/v1alpha1/clusterimagepolicy" clusterimagepolicyreconciler "github.com/sigstore/cosign/pkg/client/injection/reconciler/cosigned/v1alpha1/clusterimagepolicy" ) +const SystemNamespace = "cosign-system" + // NewController creates a Reconciler and returns the result of NewImpl. func NewController( ctx context.Context, cmw configmap.Watcher, ) *controller.Impl { clusterimagepolicyInformer := clusterimagepolicyinformer.Get(ctx) + cmInformer := configmapinformer.Get(ctx) + secretInformer := secretinformer.Get(ctx) - r := &Reconciler{} + r := &Reconciler{ + secretlister: secretInformer.Lister(), + configmaplister: cmInformer.Lister(), + kubeclient: kubeclient.Get(ctx), + } impl := clusterimagepolicyreconciler.NewImpl(ctx, r) - r.Tracker = impl.Tracker + r.tracker = impl.Tracker clusterimagepolicyInformer.Informer().AddEventHandler(controller.HandleAll(impl.Enqueue)) + // When the underlying ConfigMap changes,perform a global resync on + // ClusterImagePolicies to make sure their state is correctly reflected + // in the ConfigMap. This is admittedly a bit heavy handed, but I don't + // really see a way around it, since if something is wrong with the + // ConfigMap but there are no changes to the ClusterImagePolicy, it needs + // to be synced. + grCb := func(obj interface{}) { + logging.FromContext(ctx).Info("Doing a global resync on ClusterImagePolicies due to ConfigMap changing.") + impl.GlobalResync(clusterimagepolicyInformer.Informer()) + } + // Resync on only ConfigMap changes that pertain to the one I care about. + cmInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{ + FilterFunc: pkgreconciler.ChainFilterFuncs( + pkgreconciler.NamespaceFilterFunc(SystemNamespace), + pkgreconciler.NameFilterFunc(config.ImagePoliciesConfigName)), + Handler: controller.HandleAll(grCb), + }) return impl } diff --git a/pkg/reconciler/clusterimagepolicy/controller_test.go b/pkg/reconciler/clusterimagepolicy/controller_test.go new file mode 100644 index 000000000000..0cb90e66ed54 --- /dev/null +++ b/pkg/reconciler/clusterimagepolicy/controller_test.go @@ -0,0 +1,37 @@ +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package clusterimagepolicy + +import ( + "testing" + + "knative.dev/pkg/configmap" + . "knative.dev/pkg/reconciler/testing" + + // Fake injection informers + _ "github.com/sigstore/cosign/pkg/client/injection/informers/cosigned/v1alpha1/clusterimagepolicy/fake" + _ "knative.dev/pkg/client/injection/kube/informers/core/v1/configmap/fake" + _ "knative.dev/pkg/client/injection/kube/informers/core/v1/secret/fake" +) + +func TestNew(t *testing.T) { + ctx, _ := SetupFakeContext(t) + + c := NewController(ctx, &configmap.ManualWatcher{}) + + if c == nil { + t.Fatal("Expected NewController to return a non-nil value") + } +} diff --git a/pkg/reconciler/clusterimagepolicy/resources/configmap.go b/pkg/reconciler/clusterimagepolicy/resources/configmap.go new file mode 100644 index 000000000000..bf4d8b115fdf --- /dev/null +++ b/pkg/reconciler/clusterimagepolicy/resources/configmap.go @@ -0,0 +1,74 @@ +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package resources + +import ( + "encoding/json" + "fmt" + + "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/pkg/apis/duck" +) + +// NewConfigMap returns a new ConfigMap with an entry for the given +// ClusterImagePolicy +func NewConfigMap(ns, name string, cip *v1alpha1.ClusterImagePolicy) (*corev1.ConfigMap, error) { + entry, err := marshal(cip.Spec) + if err != nil { + return nil, err + } + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns, + Name: name, + // TODO(vaikas): Set the ownerrefs. Don't want to keep adding one + // for each CIP. + }, + Data: map[string]string{ + cip.Name: entry, + }, + } + return cm, nil +} + +// CreatePatch updates a particular entry to see if they are differing and +// returning the patch bytes for it that's suitable for calling +// ConfigMap.Patch with. +func CreatePatch(ns, name string, cm *corev1.ConfigMap, cip *v1alpha1.ClusterImagePolicy) ([]byte, error) { + entry, err := marshal(cip.Spec) + if err != nil { + return nil, err + } + after := cm.DeepCopy() + after.Data[cip.Name] = entry + jsonPatch, err := duck.CreatePatch(cm, after) + if err != nil { + return nil, fmt.Errorf("creating JSON patch: %v", err) + } + if len(jsonPatch) == 0 { + return nil, nil + } + return jsonPatch.MarshalJSON() +} + +func marshal(spec v1alpha1.ClusterImagePolicySpec) (string, error) { + bytes, err := json.Marshal(&spec) + if err != nil { + return "", err + } + return string(bytes), nil +} diff --git a/pkg/reconciler/testing/v1alpha1/clusterimagepolicy.go b/pkg/reconciler/testing/v1alpha1/clusterimagepolicy.go new file mode 100644 index 000000000000..e5fe036d2020 --- /dev/null +++ b/pkg/reconciler/testing/v1alpha1/clusterimagepolicy.go @@ -0,0 +1,51 @@ +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testing + +import ( + "context" + "time" + + "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ClusterImagePolicyOption enables further configuration of a ClusterImagePolicy. +type ClusterImagePolicyOption func(*v1alpha1.ClusterImagePolicy) + +// NewClusterImagePolicy creates a ClusterImagePolicy with ClusterImagePolicyOptions. +func NewClusterImagePolicy(name string, o ...ClusterImagePolicyOption) *v1alpha1.ClusterImagePolicy { + cip := &v1alpha1.ClusterImagePolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } + for _, opt := range o { + opt(cip) + } + cip.SetDefaults(context.Background()) + return cip +} + +func WithClusterImagePolicyDeletionTimestamp(cip *v1alpha1.ClusterImagePolicy) { + t := metav1.NewTime(time.Unix(1e9, 0)) + cip.ObjectMeta.SetDeletionTimestamp(&t) +} + +func WithImagePattern(ip v1alpha1.ImagePattern) ClusterImagePolicyOption { + return func(cip *v1alpha1.ClusterImagePolicy) { + cip.Spec.Images = append(cip.Spec.Images, ip) + } +} diff --git a/pkg/reconciler/testing/v1alpha1/factory.go b/pkg/reconciler/testing/v1alpha1/factory.go new file mode 100644 index 000000000000..6be335097c4b --- /dev/null +++ b/pkg/reconciler/testing/v1alpha1/factory.go @@ -0,0 +1,153 @@ +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testing + +import ( + "context" + "encoding/json" + "testing" + + corev1 "k8s.io/api/core/v1" + "knative.dev/pkg/configmap" + "knative.dev/pkg/logging" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + + "k8s.io/client-go/tools/record" + + "go.uber.org/zap" + ktesting "k8s.io/client-go/testing" + "knative.dev/pkg/controller" + + fakecosignclient "github.com/sigstore/cosign/pkg/client/injection/client/fake" + fakekubeclient "knative.dev/pkg/client/injection/kube/client/fake" + fakedynamicclient "knative.dev/pkg/injection/clients/dynamicclient/fake" + + "knative.dev/pkg/reconciler" + . "knative.dev/pkg/reconciler/testing" +) + +const ( + // maxEventBufferSize is the estimated max number of event notifications that + // can be buffered during reconciliation. + maxEventBufferSize = 10 +) + +// Ctor functions create a k8s controller with given params. +type Ctor func(context.Context, *Listers, configmap.Watcher) controller.Reconciler + +// MakeFactory creates a reconciler factory with fake clients and controller created by `ctor`. +func MakeFactory(ctor Ctor, unstructured bool, logger *zap.SugaredLogger) Factory { + return func(t *testing.T, r *TableRow) (controller.Reconciler, ActionRecorderList, EventList) { + ls := NewListers(r.Objects) + + var ctx context.Context + if r.Ctx != nil { + ctx = r.Ctx + } else { + ctx = context.Background() + } + ctx = logging.WithLogger(ctx, logger) + + ctx, kubeClient := fakekubeclient.With(ctx, ls.GetKubeObjects()...) + ctx, client := fakecosignclient.With(ctx, ls.GetCosignObjects()...) + ctx, dynamicClient := fakedynamicclient.With(ctx, + NewScheme(), ToUnstructured(t, r.Objects)...) + + // The dynamic client's support for patching is BS. Implement it + // here via PrependReactor (this can be overridden below by the + // provided reactors). + dynamicClient.PrependReactor("patch", "*", + func(action ktesting.Action) (bool, runtime.Object, error) { + return true, nil, nil + }) + + eventRecorder := record.NewFakeRecorder(maxEventBufferSize) + ctx = controller.WithEventRecorder(ctx, eventRecorder) + + // Check the config maps in objects and add them to the fake cm watcher + var cms []*corev1.ConfigMap + for _, obj := range r.Objects { + if cm, ok := obj.(*corev1.ConfigMap); ok { + cms = append(cms, cm) + } + } + configMapWatcher := configmap.NewStaticWatcher(cms...) + + // Set up our Controller from the fakes. + c := ctor(ctx, &ls, configMapWatcher) + + // If the reconcilers is leader aware, then promote it. + if la, ok := c.(reconciler.LeaderAware); ok { + la.Promote(reconciler.UniversalBucket(), func(reconciler.Bucket, types.NamespacedName) {}) + } + + for _, reactor := range r.WithReactors { + kubeClient.PrependReactor("*", "*", reactor) + client.PrependReactor("*", "*", reactor) + dynamicClient.PrependReactor("*", "*", reactor) + } + + // Validate all Create and Update operations + client.PrependReactor("create", "*", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) { + return ValidateCreates(ctx, action) + }) + client.PrependReactor("update", "*", func(action ktesting.Action) (handled bool, ret runtime.Object, err error) { + return ValidateUpdates(ctx, action) + }) + + actionRecorderList := ActionRecorderList{dynamicClient, client, kubeClient} + eventList := EventList{Recorder: eventRecorder} + + return c, actionRecorderList, eventList + } +} + +// ToUnstructured takes a list of k8s resources and converts them to +// Unstructured objects. +// We must pass objects as Unstructured to the dynamic client fake, or it +// won't handle them properly. +func ToUnstructured(t *testing.T, objs []runtime.Object) (us []runtime.Object) { + sch := NewScheme() + for _, obj := range objs { + obj = obj.DeepCopyObject() // Don't mess with the primary copy + // Determine and set the TypeMeta for this object based on our test scheme. + gvks, _, err := sch.ObjectKinds(obj) + if err != nil { + t.Fatal("Unable to determine kind for type:", err) + } + apiv, k := gvks[0].ToAPIVersionAndKind() + ta, err := meta.TypeAccessor(obj) + if err != nil { + t.Fatal("Unable to create type accessor:", err) + } + ta.SetAPIVersion(apiv) + ta.SetKind(k) + + b, err := json.Marshal(obj) + if err != nil { + t.Fatal("Unable to marshal:", err) + } + u := &unstructured.Unstructured{} + if err := json.Unmarshal(b, u); err != nil { + t.Fatal("Unable to unmarshal:", err) + } + us = append(us, u) + } + return +} diff --git a/pkg/reconciler/testing/v1alpha1/listers.go b/pkg/reconciler/testing/v1alpha1/listers.go new file mode 100644 index 000000000000..486a06d36ce4 --- /dev/null +++ b/pkg/reconciler/testing/v1alpha1/listers.go @@ -0,0 +1,91 @@ +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.package testing + +package testing + +import ( + "github.com/sigstore/cosign/pkg/apis/cosigned/v1alpha1" + fakecosignclientset "github.com/sigstore/cosign/pkg/client/clientset/versioned/fake" + cosignlisters "github.com/sigstore/cosign/pkg/client/listers/cosigned/v1alpha1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + fakekubeclientset "k8s.io/client-go/kubernetes/fake" + corev1listers "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" + "knative.dev/pkg/reconciler/testing" +) + +var clientSetSchemes = []func(*runtime.Scheme) error{ + fakekubeclientset.AddToScheme, + fakecosignclientset.AddToScheme, +} + +type Listers struct { + sorter testing.ObjectSorter +} + +func NewScheme() *runtime.Scheme { + scheme := runtime.NewScheme() + + for _, addTo := range clientSetSchemes { + addTo(scheme) + } + return scheme +} + +func NewListers(objs []runtime.Object) Listers { + scheme := runtime.NewScheme() + + for _, addTo := range clientSetSchemes { + addTo(scheme) + } + + ls := Listers{ + sorter: testing.NewObjectSorter(scheme), + } + + ls.sorter.AddObjects(objs...) + + return ls +} + +func (l *Listers) indexerFor(obj runtime.Object) cache.Indexer { + return l.sorter.IndexerForObjectType(obj) +} + +func (l *Listers) GetKubeObjects() []runtime.Object { + return l.sorter.ObjectsForSchemeFunc(fakekubeclientset.AddToScheme) +} + +func (l *Listers) GetCosignObjects() []runtime.Object { + return l.sorter.ObjectsForSchemeFunc(fakecosignclientset.AddToScheme) +} + +func (l *Listers) GetAllObjects() []runtime.Object { + all := l.GetCosignObjects() + all = append(all, l.GetKubeObjects()...) + return all +} + +func (l *Listers) GetClusterImagePolicyLister() cosignlisters.ClusterImagePolicyLister { + return cosignlisters.NewClusterImagePolicyLister(l.indexerFor(&v1alpha1.ClusterImagePolicy{})) +} + +func (l *Listers) GetSecretLister() corev1listers.SecretLister { + return corev1listers.NewSecretLister(l.indexerFor(&corev1.Secret{})) +} + +func (l *Listers) GetConfigMapLister() corev1listers.ConfigMapLister { + return corev1listers.NewConfigMapLister(l.indexerFor(&corev1.ConfigMap{})) +} diff --git a/third_party/VENDOR-LICENSE/LICENSE b/third_party/VENDOR-LICENSE/LICENSE new file mode 100644 index 000000000000..6a66aea5eafe --- /dev/null +++ b/third_party/VENDOR-LICENSE/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/VENDOR-LICENSE/github.com/benbjohnson/clock/LICENSE b/third_party/VENDOR-LICENSE/github.com/benbjohnson/clock/LICENSE new file mode 100644 index 000000000000..ce212cb1ceea --- /dev/null +++ b/third_party/VENDOR-LICENSE/github.com/benbjohnson/clock/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Ben Johnson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third_party/VENDOR-LICENSE/vendor/golang.org/x/crypto/LICENSE b/third_party/VENDOR-LICENSE/vendor/golang.org/x/crypto/LICENSE new file mode 100644 index 000000000000..6a66aea5eafe --- /dev/null +++ b/third_party/VENDOR-LICENSE/vendor/golang.org/x/crypto/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/VENDOR-LICENSE/vendor/golang.org/x/net/LICENSE b/third_party/VENDOR-LICENSE/vendor/golang.org/x/net/LICENSE new file mode 100644 index 000000000000..6a66aea5eafe --- /dev/null +++ b/third_party/VENDOR-LICENSE/vendor/golang.org/x/net/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/VENDOR-LICENSE/vendor/golang.org/x/text/LICENSE b/third_party/VENDOR-LICENSE/vendor/golang.org/x/text/LICENSE new file mode 100644 index 000000000000..6a66aea5eafe --- /dev/null +++ b/third_party/VENDOR-LICENSE/vendor/golang.org/x/text/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.