diff --git a/sdk/contract.md b/sdk/contract.md index 42102378..696d142f 100644 --- a/sdk/contract.md +++ b/sdk/contract.md @@ -26,6 +26,10 @@ WriteOutput writes the content to the specifies file at the path /kratix/output/ WriteStatus writes the specified status to the `/kratix/metadata/status.yaml` +**`WriteLabels(map[string]string) error`** + +WriteLabels writes the specified labels to the `/kratix/metadata/labels.yaml`. These labels will be merged with the existing resource labels after the pipeline completes. + **`WriteDestinationSelectors([]DestinationSelector) error`** WriteDestinationSelectors writes the specified Destination Selectors to the `/kratix/metadata/destination_selectors.yaml` @@ -70,6 +74,10 @@ PublishStatus updates the status of the provided resource with the provided stat ReadStatus reads the /kratix/metadata/status.yaml +**`ReadLabels() (map[string]string, error)`** + +ReadLabels reads the /kratix/metadata/labels.yaml + ## Promise Interface The SDK interface implements the core functions for interacting with a Promise diff --git a/work-creator/cmd/update_status.go b/work-creator/cmd/update_status.go index ade29fa1..f54051a4 100644 --- a/work-creator/cmd/update_status.go +++ b/work-creator/cmd/update_status.go @@ -16,7 +16,7 @@ import ( func updateStatusCmd() *cobra.Command { cmd := &cobra.Command{ Use: "update-status", - Short: "Update status of Kubernetes resources", + Short: "Update status and labels of Kubernetes resources", RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() return runUpdateStatus(ctx) @@ -29,6 +29,7 @@ func updateStatusCmd() *cobra.Command { func runUpdateStatus(ctx context.Context) error { workspaceDir := "/work-creator-files" statusFile := filepath.Join(workspaceDir, "metadata", "status.yaml") + labelsFile := filepath.Join(workspaceDir, "metadata", "labels.yaml") params := helpers.GetParametersFromEnv() @@ -44,6 +45,32 @@ func runUpdateStatus(ctx context.Context) error { return fmt.Errorf("failed to get existing object: %w", err) } + // Handle labels update + incomingLabels, err := readLabelsFile(labelsFile) + if err != nil { + return fmt.Errorf("failed to load incoming labels: %w", err) + } + + if len(incomingLabels) > 0 { + existingLabels := existingObj.GetLabels() + if existingLabels == nil { + existingLabels = map[string]string{} + } + mergedLabels := lib.MergeLabels(existingLabels, incomingLabels) + existingObj.SetLabels(mergedLabels) + + if _, err = objectClient.Update(ctx, existingObj, metav1.UpdateOptions{}); err != nil { + return fmt.Errorf("failed to update labels: %w", err) + } + + // Re-fetch the object after labels update to get the latest resourceVersion + existingObj, err = objectClient.Get(ctx, params.ObjectName, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get existing object after labels update: %w", err) + } + } + + // Handle status update existingStatus := map[string]any{} if existingObj.Object["status"] != nil { existingStatus = existingObj.Object["status"].(map[string]any) @@ -84,3 +111,17 @@ func readStatusFile(statusFile string) (map[string]any, error) { } return incomingStatus, nil } + +func readLabelsFile(labelsFile string) (map[string]string, error) { + incomingLabels := map[string]string{} + if _, err := os.Stat(labelsFile); err == nil { + incomingLabelsBytes, err := os.ReadFile(labelsFile) + if err != nil { + return nil, fmt.Errorf("failed to read labels file: %w", err) + } + if err := yaml.Unmarshal(incomingLabelsBytes, &incomingLabels); err != nil { + return nil, fmt.Errorf("failed to unmarshal incoming labels: %w", err) + } + } + return incomingLabels, nil +} diff --git a/work-creator/lib/status_updater.go b/work-creator/lib/status_updater.go index f4ac7d77..6cdc421f 100644 --- a/work-creator/lib/status_updater.go +++ b/work-creator/lib/status_updater.go @@ -19,6 +19,25 @@ func MergeStatuses(existing map[string]any, incoming map[string]any) map[string] return mergeRecursive(existing, incoming) } +// MergeLabels takes two label maps and returns a new label map that is a +// merge of the two. If a key exists in both maps, the value from the incoming +// map will be used. +func MergeLabels(existing map[string]string, incoming map[string]string) map[string]string { + result := make(map[string]string) + + // First, copy all keys from existing + for k, v := range existing { + result[k] = v + } + + // Then merge or overwrite with incoming + for k, v := range incoming { + result[k] = v + } + + return result +} + // MarkAsCompleted takes a status map and returns a new status map with the // "ConfigureWorkflowCompleted" condition set to true. It will also update the // "message" field to "Resource requested" if the message is currently diff --git a/work-creator/lib/status_updater_test.go b/work-creator/lib/status_updater_test.go index 9c7d7f29..b1af3efe 100644 --- a/work-creator/lib/status_updater_test.go +++ b/work-creator/lib/status_updater_test.go @@ -9,6 +9,53 @@ import ( var _ = Describe("StatusUpdater", func() { + Describe("MergeLabels", func() { + It("merges the two maps", func() { + existing := map[string]string{ + "existing-key": "existing-value", + "shared-key": "old-value", + } + incoming := map[string]string{ + "incoming-key": "incoming-value", + "shared-key": "new-value", + } + result := lib.MergeLabels(existing, incoming) + Expect(result).To(SatisfyAll( + HaveKeyWithValue("existing-key", "existing-value"), + HaveKeyWithValue("incoming-key", "incoming-value"), + HaveKeyWithValue("shared-key", "new-value"), + )) + Expect(result).To(HaveLen(3)) + }) + + It("returns incoming labels when existing is empty", func() { + existing := map[string]string{} + incoming := map[string]string{ + "key1": "value1", + "key2": "value2", + } + result := lib.MergeLabels(existing, incoming) + Expect(result).To(Equal(incoming)) + }) + + It("returns existing labels when incoming is empty", func() { + existing := map[string]string{ + "key1": "value1", + "key2": "value2", + } + incoming := map[string]string{} + result := lib.MergeLabels(existing, incoming) + Expect(result).To(Equal(existing)) + }) + + It("returns empty map when both are empty", func() { + existing := map[string]string{} + incoming := map[string]string{} + result := lib.MergeLabels(existing, incoming) + Expect(result).To(BeEmpty()) + }) + }) + Describe("MergeStatuses", func() { It("merges the two maps", func() { existing := map[string]any{