diff --git a/function.go b/function.go index c1558e96..9fd7a8b6 100644 --- a/function.go +++ b/function.go @@ -11,6 +11,8 @@ import ( "time" "gopkg.in/yaml.v2" + fnlabels "knative.dev/kn-plugin-func/k8s/labels" + "knative.dev/pkg/ptr" ) // FunctionFile is the file used for the serialized form of a function. @@ -344,6 +346,61 @@ func (f Function) ImageWithDigest() string { return part1 + strings.Split(part2, ":")[0] + "@" + f.ImageDigest } +// LabelsMap combines default labels with the labels slice provided. +// It will the resulting slice with ValidateLabels and return a key/value map. +// - key: EXAMPLE1 # Label directly from a value +// value: value1 +// - key: EXAMPLE2 # Label from the local ENV var +// value: {{ env:MY_ENV }} +func (f Function) LabelsMap() (map[string]string, error) { + defaultLabels := []Label{ + { + Key: ptr.String(fnlabels.FunctionKey), + Value: ptr.String(fnlabels.FunctionValue), + }, + { + Key: ptr.String(fnlabels.FunctionNameKey), + Value: ptr.String(f.Name), + }, + { + Key: ptr.String(fnlabels.FunctionRuntimeKey), + Value: ptr.String(f.Runtime), + }, + // --- handle usage of deprecated labels (`boson.dev/function`, `boson.dev/runtime`) + { + Key: ptr.String(fnlabels.DeprecatedFunctionKey), + Value: ptr.String(fnlabels.FunctionValue), + }, + { + Key: ptr.String(fnlabels.DeprecatedFunctionRuntimeKey), + Value: ptr.String(f.Runtime), + }, + // --- end of handling usage of deprecated runtime labels + } + + labels := append(defaultLabels, f.Labels...) + if err := ValidateLabels(labels); len(err) != 0 { + return nil, errors.New(strings.Join(err, " ")) + } + + l := map[string]string{} + for _, label := range labels { + if label.Value == nil { + l[*label.Key] = "" + } else { + if strings.HasPrefix(*label.Value, "{{") { + // env variable format is validated above in ValidateLabels + match := regLocalEnv.FindStringSubmatch(*label.Value) + l[*label.Key] = os.Getenv(match[1]) + } else { + l[*label.Key] = *label.Value + } + } + } + + return l, nil +} + // assertEmptyRoot ensures that the directory is empty enough to be used for // initializing a new function. func assertEmptyRoot(path string) (err error) { diff --git a/function_unit_test.go b/function_unit_test.go index b433ac1c..4729bfe3 100644 --- a/function_unit_test.go +++ b/function_unit_test.go @@ -4,7 +4,11 @@ package function import ( + "reflect" "testing" + + fnlabels "knative.dev/kn-plugin-func/k8s/labels" + . "knative.dev/kn-plugin-func/testing" ) func TestFunction_ImageWithDigest(t *testing.T) { @@ -82,3 +86,126 @@ func TestFunction_ImageName(t *testing.T) { } } } + +func Test_LabelsMap(t *testing.T) { + key1 := "key1" + key2 := "key2" + value1 := "value1" + value2 := "value2" + + defer WithEnvVar(t, "BAD_EXAMPLE", ":invalid")() + valueLocalEnvIncorrect4 := "{{env:BAD_EXAMPLE}}" + + defer WithEnvVar(t, "GOOD_EXAMPLE", "valid")() + valueLocalEnv4 := "{{env:GOOD_EXAMPLE}}" + + tests := []struct { + name string + labels []Label + expectErr bool + expectedMap map[string]string + }{ + { + name: "invalid Labels should return err", + labels: []Label{ + { + Value: &value1, + }, + }, + expectErr: true, + }, + { + name: "with valid env var", + labels: []Label{ + { + Key: &key1, + Value: &valueLocalEnv4, + }, + }, + expectErr: false, + expectedMap: map[string]string{ + key1: "valid", + }, + }, + { + name: "with invalid env var", + labels: []Label{ + { + Key: &key1, + Value: &valueLocalEnvIncorrect4, + }, + }, + expectErr: true, + }, + { + name: "empty labels allowed. returns default labels", + labels: []Label{ + { + Key: &key1, + }, + }, + expectErr: false, + expectedMap: map[string]string{ + key1: "", + }, + }, + { + name: "full set of labels", + labels: []Label{ + { + Key: &key1, + Value: &value1, + }, + { + Key: &key2, + Value: &value2, + }, + }, + expectErr: false, + expectedMap: map[string]string{ + key1: value1, + key2: value2, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := Function{ + Name: "some-function", + Runtime: "golang", + Labels: tt.labels, + } + got, err := f.LabelsMap() + + if tt.expectErr { + if err == nil { + t.Error("expected error but didn't get an error from LabelsMap") + } + } else { + if err != nil { + t.Errorf("got unexpected err: %s", err) + } + } + if err == nil { + defaultLabels := expectedDefaultLabels(f) + for k, v := range defaultLabels { + tt.expectedMap[k] = v + } + if res := reflect.DeepEqual(got, tt.expectedMap); !res { + t.Errorf("mismatch in actual and expected labels return. actual: %#v, expected: %#v", got, tt.expectedMap) + } + } + }) + } +} + +func expectedDefaultLabels(f Function) map[string]string { + return map[string]string{ + fnlabels.FunctionKey: fnlabels.FunctionValue, + fnlabels.FunctionNameKey: f.Name, + fnlabels.FunctionRuntimeKey: f.Runtime, + fnlabels.DeprecatedFunctionKey: fnlabels.FunctionValue, + fnlabels.DeprecatedFunctionRuntimeKey: f.Runtime, + } +} diff --git a/k8s/persistent_volumes.go b/k8s/persistent_volumes.go index c0a3a61c..cf685305 100644 --- a/k8s/persistent_volumes.go +++ b/k8s/persistent_volumes.go @@ -17,7 +17,7 @@ func GetPersistentVolumeClaim(ctx context.Context, name, namespaceOverride strin return client.CoreV1().PersistentVolumeClaims(namespace).Get(ctx, name, metav1.GetOptions{}) } -func CreatePersistentVolumeClaim(ctx context.Context, name, namespaceOverride string, labels map[string]string, accessMode corev1.PersistentVolumeAccessMode, resourceRequest resource.Quantity) (err error) { +func CreatePersistentVolumeClaim(ctx context.Context, name, namespaceOverride string, labels map[string]string, annotations map[string]string, accessMode corev1.PersistentVolumeAccessMode, resourceRequest resource.Quantity) (err error) { client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride) if err != nil { return @@ -25,9 +25,10 @@ func CreatePersistentVolumeClaim(ctx context.Context, name, namespaceOverride st pvc := &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, + Name: name, + Namespace: namespace, + Labels: labels, + Annotations: annotations, }, Spec: corev1.PersistentVolumeClaimSpec{ AccessModes: []corev1.PersistentVolumeAccessMode{accessMode}, diff --git a/k8s/secrets.go b/k8s/secrets.go index 0fd8780b..7884234f 100644 --- a/k8s/secrets.go +++ b/k8s/secrets.go @@ -83,7 +83,7 @@ func DeleteSecrets(ctx context.Context, namespaceOverride string, listOptions me return client.CoreV1().Secrets(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOptions) } -func EnsureDockerRegistrySecretExist(ctx context.Context, name, namespaceOverride string, labels map[string]string, username, password, server string) (err error) { +func EnsureDockerRegistrySecretExist(ctx context.Context, name, namespaceOverride string, labels map[string]string, annotations map[string]string, username, password, server string) (err error) { client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride) if err != nil { return @@ -110,9 +110,10 @@ func EnsureDockerRegistrySecretExist(ctx context.Context, name, namespaceOverrid if createSecret || !bytes.Equal(currentSecret.Data[secretKey], dockerConfigJSONContent) { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, + Name: name, + Namespace: namespace, + Labels: labels, + Annotations: annotations, }, Type: corev1.SecretTypeOpaque, Data: map[string][]byte{}, diff --git a/knative/deployer.go b/knative/deployer.go index 75996cc6..7bacf2d2 100644 --- a/knative/deployer.go +++ b/knative/deployer.go @@ -22,7 +22,6 @@ import ( fn "knative.dev/kn-plugin-func" "knative.dev/kn-plugin-func/k8s" - "knative.dev/kn-plugin-func/k8s/labels" ) const LIVENESS_ENDPOINT = "/health/liveness" @@ -294,10 +293,13 @@ func generateNewService(f fn.Function, decorator DeployDecorator) (*v1.Service, } container.VolumeMounts = newVolumeMounts - labels, err := processLabels(f, decorator) + labels, err := f.LabelsMap() if err != nil { return nil, err } + if decorator != nil { + labels = decorator.UpdateLabels(f, labels) + } annotations := f.Annotations if decorator != nil { @@ -374,10 +376,14 @@ func updateService(f fn.Function, newEnv []corev1.EnvVar, newEnvFrom []corev1.En return service, err } - labels, err := processLabels(f, decorator) + labels, err := f.LabelsMap() if err != nil { - return service, err + return nil, err } + if decorator != nil { + labels = decorator.UpdateLabels(f, labels) + } + service.ObjectMeta.Labels = labels service.Spec.Template.ObjectMeta.Labels = labels @@ -395,54 +401,6 @@ func updateService(f fn.Function, newEnv []corev1.EnvVar, newEnvFrom []corev1.En } } -// processLabels generates a map of labels as key/value pairs from a function config -// labels: -// - key: EXAMPLE1 # Label directly from a value -// value: value1 -// - key: EXAMPLE2 # Label from the local ENV var -// value: {{ env:MY_ENV }} -func processLabels(f fn.Function, decorator DeployDecorator) (map[string]string, error) { - labels := map[string]string{ - labels.FunctionKey: labels.FunctionValue, - labels.FunctionNameKey: f.Name, - labels.FunctionRuntimeKey: f.Runtime, - - // --- handle usage of deprecated labels (`boson.dev/function`, `boson.dev/runtime`) - labels.DeprecatedFunctionKey: labels.FunctionValue, - labels.DeprecatedFunctionRuntimeKey: f.Runtime, - // --- end of handling usage of deprecated runtime labels - } - - if decorator != nil { - labels = decorator.UpdateLabels(f, labels) - } - - for _, label := range f.Labels { - if label.Key != nil && label.Value != nil { - if strings.HasPrefix(*label.Value, "{{") { - slices := strings.Split(strings.Trim(*label.Value, "{} "), ":") - if len(slices) == 2 { - // label from the local ENV var, eg. author={{ env:$USER }} - localValue, err := processLocalEnvValue(*label.Value) - if err != nil { - return nil, err - } - labels[*label.Key] = localValue - continue - } - } else { - // a standard label with key and value, eg. author=alice@example.com - labels[*label.Key] = *label.Value - continue - } - } else if label.Key != nil && label.Value == nil { - labels[*label.Key] = "" - } - } - - return labels, nil -} - // processEnvs generates array of EnvVars and EnvFromSources from a function config // envs: // - name: EXAMPLE1 # ENV directly from a value diff --git a/pipelines/tekton/pipeplines_provider.go b/pipelines/tekton/pipeplines_provider.go index 03293ebd..ed12f6ec 100644 --- a/pipelines/tekton/pipeplines_provider.go +++ b/pipelines/tekton/pipeplines_provider.go @@ -19,7 +19,7 @@ import ( fn "knative.dev/kn-plugin-func" "knative.dev/kn-plugin-func/docker" "knative.dev/kn-plugin-func/k8s" - "knative.dev/kn-plugin-func/k8s/labels" + fnlabels "knative.dev/kn-plugin-func/k8s/labels" "knative.dev/kn-plugin-func/knative" "knative.dev/pkg/apis" ) @@ -97,12 +97,15 @@ func (pp *PipelinesProvider) Run(ctx context.Context, f fn.Function) error { pp.namespace = namespace // let's specify labels that will be applied to every resouce that is created for a Pipeline - labels := map[string]string{labels.FunctionNameKey: f.Name} + labels, err := f.LabelsMap() + if err != nil { + return err + } if pp.decorator != nil { labels = pp.decorator.UpdateLabels(f, labels) } - err = k8s.CreatePersistentVolumeClaim(ctx, getPipelinePvcName(f), pp.namespace, labels, corev1.ReadWriteOnce, *resource.NewQuantity(DefaultPersistentVolumeClaimSize, resource.DecimalSI)) + err = k8s.CreatePersistentVolumeClaim(ctx, getPipelinePvcName(f), pp.namespace, labels, f.Annotations, corev1.ReadWriteOnce, *resource.NewQuantity(DefaultPersistentVolumeClaimSize, resource.DecimalSI)) if err != nil { if !errors.IsAlreadyExists(err) { return fmt.Errorf("problem creating persistent volume claim: %v", err) @@ -135,7 +138,7 @@ func (pp *PipelinesProvider) Run(ctx context.Context, f fn.Function) error { registry = authn.DefaultAuthKey } - err = k8s.EnsureDockerRegistrySecretExist(ctx, getPipelineSecretName(f), pp.namespace, labels, creds.Username, creds.Password, registry) + err = k8s.EnsureDockerRegistrySecretExist(ctx, getPipelineSecretName(f), pp.namespace, labels, f.Annotations, creds.Username, creds.Password, registry) if err != nil { return fmt.Errorf("problem in creating secret: %v", err) } @@ -182,7 +185,7 @@ func (pp *PipelinesProvider) Run(ctx context.Context, f fn.Function) error { func (pp *PipelinesProvider) Remove(ctx context.Context, f fn.Function) error { - l := k8slabels.SelectorFromSet(k8slabels.Set(map[string]string{labels.FunctionNameKey: f.Name})) + l := k8slabels.SelectorFromSet(k8slabels.Set(map[string]string{fnlabels.FunctionNameKey: f.Name})) listOptions := metav1.ListOptions{ LabelSelector: l.String(), } diff --git a/pipelines/tekton/resources.go b/pipelines/tekton/resources.go index ac6bb3b4..394e5423 100644 --- a/pipelines/tekton/resources.go +++ b/pipelines/tekton/resources.go @@ -98,8 +98,9 @@ func generatePipeline(f fn.Function, labels map[string]string) *pplnv1beta1.Pipe return &pplnv1beta1.Pipeline{ ObjectMeta: v1.ObjectMeta{ - Name: pipelineName, - Labels: labels, + Name: pipelineName, + Labels: labels, + Annotations: f.Annotations, }, Spec: pplnv1beta1.PipelineSpec{ Params: params, @@ -200,6 +201,7 @@ func generatePipelineRun(f fn.Function, labels map[string]string) *pplnv1beta1.P ObjectMeta: v1.ObjectMeta{ GenerateName: fmt.Sprintf("%s-run-", getPipelineName(f)), Labels: labels, + Annotations: f.Annotations, }, Spec: pplnv1beta1.PipelineRunSpec{ PipelineRef: &pplnv1beta1.PipelineRef{