Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions clm/cmd/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"os"
"time"

"github.com/sap/go-generics/slices"
"github.com/spf13/cobra"

corev1 "k8s.io/api/core/v1"
Expand All @@ -24,7 +25,6 @@ import (
"github.com/sap/component-operator-runtime/clm/internal/release"
"github.com/sap/component-operator-runtime/pkg/component"
"github.com/sap/component-operator-runtime/pkg/reconciler"
"github.com/sap/go-generics/slices"
)

const applyUsage = `Apply component manifests to Kubernetes cluster`
Expand Down Expand Up @@ -117,7 +117,7 @@ func newApplyCmd() *cobra.Command {

for {
release.State = component.StateProcessing
ok, err := reconciler.Apply(context.TODO(), &release.Inventory, objects, namespace, ownerId, fmt.Sprintf("%d", release.Revision))
ok, err := reconciler.Apply(context.TODO(), &release.Inventory, objects, namespace, ownerId, release.GetDigest())
if err != nil {
if !isEphmeralError(err) || errCount >= maxErrCount {
return err
Expand Down
2 changes: 1 addition & 1 deletion clm/cmd/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import (
"os"
"time"

"github.com/sap/go-generics/slices"
"github.com/spf13/cobra"

"github.com/sap/component-operator-runtime/clm/internal/backoff"
"github.com/sap/component-operator-runtime/clm/internal/release"
"github.com/sap/component-operator-runtime/pkg/component"
"github.com/sap/component-operator-runtime/pkg/reconciler"
"github.com/sap/go-generics/slices"
)

const deleteUsage = `Delete component from Kubernetes cluster`
Expand Down
1 change: 1 addition & 0 deletions clm/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ func newRootCmd() *cobra.Command {
newDeleteCmd(),
newStatusCmd(),
newListCmd(),
newTemplateCmd(),
)

return cmd
Expand Down
88 changes: 88 additions & 0 deletions clm/cmd/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and component-operator-runtime contributors
SPDX-License-Identifier: Apache-2.0
*/

package cmd

import (
"context"
"fmt"
"time"

"github.com/sap/go-generics/slices"
"github.com/spf13/cobra"

kyaml "sigs.k8s.io/yaml"

"github.com/sap/component-operator-runtime/clm/internal/manifests"
"github.com/sap/component-operator-runtime/clm/internal/release"
)

const templateUsage = `Render component manifests to standard output without applying them to the cluster`

type templateOptions struct {
valuesSources []string
}

func newTemplateCmd() *cobra.Command {
options := &templateOptions{}

cmd := &cobra.Command{
Use: "template NAME SOURCE...",
Short: "Render component",
Long: templateUsage,
SilenceUsage: true,
Args: cobra.MinimumNArgs(2),
PreRunE: func(c *cobra.Command, args []string) error {
return nil
},
RunE: func(c *cobra.Command, args []string) (err error) {
name := args[0]
manifestSources := args[1:]
namespace := c.Flag("namespace").Value.String()

clnt, err := getClient(c.Flag("kubeconfig").Value.String())
if err != nil {
return err
}

release := release.NewRelease(namespace, name)
release.Revision += 1

objects, err := manifests.Generate(manifestSources, options.valuesSources, fullName, clnt, release)
if err != nil {
return err
}

for _, object := range objects {
fmt.Printf("---\n%s", must(kyaml.Marshal(object)))
}

return nil
},
ValidArgsFunction: func(c *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) > 0 {
return nil, cobra.ShellCompDirectiveDefault
}
if clnt, err := getClient(c.Flag("kubeconfig").Value.String()); err == nil {
releaseClient := release.NewClient(fullName, clnt)
namespace := c.Flag("namespace").Value.String()
if namespace == "" {
namespace = "default"
}
ctx, cancel := context.WithTimeout(context.TODO(), 3*time.Second)
defer cancel()
if releases, err := releaseClient.List(ctx, namespace); err == nil {
return slices.Collect(releases, func(release *release.Release) string { return release.GetName() }), cobra.ShellCompDirectiveNoFileComp
}
}
return nil, cobra.ShellCompDirectiveDefault
},
}

flags := cmd.Flags()
flags.StringArrayVarP(&options.valuesSources, "values", "f", nil, "Path to values file in yaml format (can be repeated, values will be merged in order of appearance)")

return cmd
}
58 changes: 41 additions & 17 deletions clm/internal/manifests/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ SPDX-License-Identifier: Apache-2.0
package manifests

import (
"encoding/json"

apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/sap/component-operator-runtime/clm/internal/release"
Expand All @@ -15,8 +18,19 @@ import (

type Component struct {
metav1.PartialObjectMetadata
release *release.Release
values map[string]any
Spec ComponentSpec `json:"spec"`
Status ComponentStatus `json:"status"`
values map[string]any
}

type ComponentSpec struct {
Values *apiextensionsv1.JSON `json:"values,omitempty"`
}

type ComponentStatus struct {
component.Status `json:",inline"`
LastAttemptedDigest string `json:"lastAttemptedDigest,omitempty"`
LastAttemptedRevision string `json:"lastAttemptedRevision,omitempty"`
}

var _ component.Component = &Component{}
Expand All @@ -26,19 +40,7 @@ func (c *Component) GetSpec() types.Unstructurable {
}

func (c *Component) GetStatus() *component.Status {
return &component.Status{
// TODO: populate missing fields
// ObservedGeneration
// AppliedGeneration
// LastObservedAt
// LastAppliedAt
// ProcessingDigest
// ProcessingSince
Revision: c.release.Revision,
// Conditions
State: c.release.State,
Inventory: c.release.Inventory,
}
return &c.Status.Status
}

func componentFromRelease(release *release.Release, values map[string]any) *Component {
Expand All @@ -54,7 +56,29 @@ func componentFromRelease(release *release.Release, values map[string]any) *Comp
Name: release.GetName(),
},
},
release: release,
values: values,
Spec: ComponentSpec{
Values: &apiextensionsv1.JSON{
Raw: must(json.Marshal(values)),
},
},
Status: ComponentStatus{
Status: component.Status{
// TODO: populate missing fields
// ObservedGeneration
// AppliedGeneration
// LastObservedAt
// LastAppliedAt
ProcessingDigest: release.GetDigest(),
// ProcessingSince
// LastProcessingDigest
Revision: release.Revision,
// Conditions
State: release.State,
Inventory: release.Inventory,
},
LastAttemptedDigest: release.GetDigest(),
LastAttemptedRevision: release.GetDigest(),
},
values: values,
}
}
46 changes: 26 additions & 20 deletions clm/internal/manifests/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,39 +47,45 @@ func Generate(manifestSources []string, valuesSources []string, reconcilerName s

for _, source := range manifestSources {
// TODO: support helm, oci URLs
path := source

if info, err := os.Stat(path); err != nil {
var fsys fs.FS
var path string

source, err := filepath.Abs(source)
if err != nil {
return nil, err
}

if info, err := os.Stat(source); err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("no such file or directory: %s", path)
return nil, fmt.Errorf("no such file or directory: %s", source)
} else {
return nil, err
}
} else if !info.IsDir() {
} else if info.IsDir() {
fsys = os.DirFS("/")
path = source[1:]
} else {
tmpdir, err := os.MkdirTemp("", "clm-")
if err != nil {
return nil, err
}
defer os.RemoveAll(tmpdir)
if _, err := copyFile(path, fmt.Sprintf("%s/%s", tmpdir, "resources.yaml")); err != nil {
if _, err := copyFile(source, fmt.Sprintf("%s/%s", tmpdir, "resources.yaml")); err != nil {
return nil, err
}
path = tmpdir
fsys = os.DirFS(tmpdir)
path = ""
}
path, err := filepath.Abs(path)
if err != nil {
return nil, err
}
fsys := os.DirFS(path)

var generator manifests.Generator
if _, err = fs.Stat(fsys, "Chart.yaml"); err == nil {
generator, err = helm.NewHelmGenerator(fsys, "", nil)
if _, err = fs.Stat(fsys, filepath.Clean(path+"/Chart.yaml")); err == nil {
generator, err = helm.NewHelmGenerator(fsys, path, nil)
if err != nil {
return nil, err
}
} else if errors.Is(err, fs.ErrNotExist) {
generator, err = kustomize.NewKustomizeGenerator(fsys, "", nil, kustomize.KustomizeGeneratorOptions{})
generator, err = kustomize.NewKustomizeGenerator(fsys, path, nil, kustomize.KustomizeGeneratorOptions{})
if err != nil {
return nil, err
}
Expand All @@ -88,17 +94,17 @@ func Generate(manifestSources []string, valuesSources []string, reconcilerName s
}

releaseComponent := componentFromRelease(release, allValues)
// TODO: what about component digest

generateCtx := component.NewContext(context.TODO()).
WithReconcilerName(reconcilerName).
WithLocalClient(clnt).
WithClient(clnt).
WithComponent(releaseComponent).
WithComponentName(releaseComponent.GetName()).
WithComponentNamespace(releaseComponent.GetNamespace()).
WithComponentDigest("").
WithComponentRevision(releaseComponent.GetStatus().Revision)
objects, err := generator.Generate(generateCtx, release.GetNamespace(), release.GetName(), types.UnstructurableMap(allValues))
WithComponentName(releaseComponent.Name).
WithComponentNamespace(releaseComponent.Namespace).
WithComponentDigest(releaseComponent.Status.ProcessingDigest).
WithComponentRevision(releaseComponent.Status.Revision)
objects, err := generator.Generate(generateCtx, releaseComponent.Namespace, releaseComponent.Name, types.UnstructurableMap(allValues))
if err != nil {
return nil, err
}
Expand Down
9 changes: 9 additions & 0 deletions clm/internal/manifests/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ import (
"os"
)

// TODO: consolidate all the util files into an internal reuse package

func must[T any](x T, err error) T {
if err != nil {
panic(err)
}
return x
}

func copyFile(src, dst string) (int64, error) {
sourceFileStat, err := os.Stat(src)
if err != nil {
Expand Down
12 changes: 12 additions & 0 deletions clm/internal/release/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ SPDX-License-Identifier: Apache-2.0
package release

import (
"fmt"
"strconv"
"time"

Expand Down Expand Up @@ -36,6 +37,13 @@ type Release struct {
State component.State
}

func NewRelease(namespace string, name string) *Release {
return &Release{
namespace: namespace,
name: name,
}
}

func (r *Release) GetNamespace() string {
return r.namespace
}
Expand All @@ -44,6 +52,10 @@ func (r *Release) GetName() string {
return r.name
}

func (r *Release) GetDigest() string {
return sha256hex([]byte(fmt.Sprintf("%d", r.Revision)))
}

func (r *Release) IsDeleting() bool {
return !r.configMap.DeletionTimestamp.IsZero()
}
Expand Down
18 changes: 18 additions & 0 deletions clm/internal/release/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and component-operator-runtime contributors
SPDX-License-Identifier: Apache-2.0
*/

package release

import (
"crypto/sha256"
"encoding/hex"
)

// TODO: consolidate all the util files into an internal reuse package

func sha256hex(data []byte) string {
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:])
}
Loading