Skip to content

Commit e058bfa

Browse files
authored
add subcommand clm template (#437)
1 parent cf94a5c commit e058bfa

10 files changed

Lines changed: 198 additions & 80 deletions

File tree

clm/cmd/apply.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"os"
1313
"time"
1414

15+
"github.com/sap/go-generics/slices"
1516
"github.com/spf13/cobra"
1617

1718
corev1 "k8s.io/api/core/v1"
@@ -24,7 +25,6 @@ import (
2425
"github.com/sap/component-operator-runtime/clm/internal/release"
2526
"github.com/sap/component-operator-runtime/pkg/component"
2627
"github.com/sap/component-operator-runtime/pkg/reconciler"
27-
"github.com/sap/go-generics/slices"
2828
)
2929

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

118118
for {
119119
release.State = component.StateProcessing
120-
ok, err := reconciler.Apply(context.TODO(), &release.Inventory, objects, namespace, ownerId, fmt.Sprintf("%d", release.Revision))
120+
ok, err := reconciler.Apply(context.TODO(), &release.Inventory, objects, namespace, ownerId, release.GetDigest())
121121
if err != nil {
122122
if !isEphmeralError(err) || errCount >= maxErrCount {
123123
return err

clm/cmd/delete.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ import (
1212
"os"
1313
"time"
1414

15+
"github.com/sap/go-generics/slices"
1516
"github.com/spf13/cobra"
1617

1718
"github.com/sap/component-operator-runtime/clm/internal/backoff"
1819
"github.com/sap/component-operator-runtime/clm/internal/release"
1920
"github.com/sap/component-operator-runtime/pkg/component"
2021
"github.com/sap/component-operator-runtime/pkg/reconciler"
21-
"github.com/sap/go-generics/slices"
2222
)
2323

2424
const deleteUsage = `Delete component from Kubernetes cluster`

clm/cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ func newRootCmd() *cobra.Command {
6464
newDeleteCmd(),
6565
newStatusCmd(),
6666
newListCmd(),
67+
newTemplateCmd(),
6768
)
6869

6970
return cmd

clm/cmd/template.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and component-operator-runtime contributors
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package cmd
7+
8+
import (
9+
"context"
10+
"fmt"
11+
"time"
12+
13+
"github.com/sap/go-generics/slices"
14+
"github.com/spf13/cobra"
15+
16+
kyaml "sigs.k8s.io/yaml"
17+
18+
"github.com/sap/component-operator-runtime/clm/internal/manifests"
19+
"github.com/sap/component-operator-runtime/clm/internal/release"
20+
)
21+
22+
const templateUsage = `Render component manifests to standard output without applying them to the cluster`
23+
24+
type templateOptions struct {
25+
valuesSources []string
26+
}
27+
28+
func newTemplateCmd() *cobra.Command {
29+
options := &templateOptions{}
30+
31+
cmd := &cobra.Command{
32+
Use: "template NAME SOURCE...",
33+
Short: "Render component",
34+
Long: templateUsage,
35+
SilenceUsage: true,
36+
Args: cobra.MinimumNArgs(2),
37+
PreRunE: func(c *cobra.Command, args []string) error {
38+
return nil
39+
},
40+
RunE: func(c *cobra.Command, args []string) (err error) {
41+
name := args[0]
42+
manifestSources := args[1:]
43+
namespace := c.Flag("namespace").Value.String()
44+
45+
clnt, err := getClient(c.Flag("kubeconfig").Value.String())
46+
if err != nil {
47+
return err
48+
}
49+
50+
release := release.NewRelease(namespace, name)
51+
release.Revision += 1
52+
53+
objects, err := manifests.Generate(manifestSources, options.valuesSources, fullName, clnt, release)
54+
if err != nil {
55+
return err
56+
}
57+
58+
for _, object := range objects {
59+
fmt.Printf("---\n%s", must(kyaml.Marshal(object)))
60+
}
61+
62+
return nil
63+
},
64+
ValidArgsFunction: func(c *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
65+
if len(args) > 0 {
66+
return nil, cobra.ShellCompDirectiveDefault
67+
}
68+
if clnt, err := getClient(c.Flag("kubeconfig").Value.String()); err == nil {
69+
releaseClient := release.NewClient(fullName, clnt)
70+
namespace := c.Flag("namespace").Value.String()
71+
if namespace == "" {
72+
namespace = "default"
73+
}
74+
ctx, cancel := context.WithTimeout(context.TODO(), 3*time.Second)
75+
defer cancel()
76+
if releases, err := releaseClient.List(ctx, namespace); err == nil {
77+
return slices.Collect(releases, func(release *release.Release) string { return release.GetName() }), cobra.ShellCompDirectiveNoFileComp
78+
}
79+
}
80+
return nil, cobra.ShellCompDirectiveDefault
81+
},
82+
}
83+
84+
flags := cmd.Flags()
85+
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)")
86+
87+
return cmd
88+
}

clm/internal/manifests/component.go

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ SPDX-License-Identifier: Apache-2.0
66
package manifests
77

88
import (
9+
"encoding/json"
10+
11+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
912
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1013

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

1619
type Component struct {
1720
metav1.PartialObjectMetadata
18-
release *release.Release
19-
values map[string]any
21+
Spec ComponentSpec `json:"spec"`
22+
Status ComponentStatus `json:"status"`
23+
values map[string]any
24+
}
25+
26+
type ComponentSpec struct {
27+
Values *apiextensionsv1.JSON `json:"values,omitempty"`
28+
}
29+
30+
type ComponentStatus struct {
31+
component.Status `json:",inline"`
32+
LastAttemptedDigest string `json:"lastAttemptedDigest,omitempty"`
33+
LastAttemptedRevision string `json:"lastAttemptedRevision,omitempty"`
2034
}
2135

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

2842
func (c *Component) GetStatus() *component.Status {
29-
return &component.Status{
30-
// TODO: populate missing fields
31-
// ObservedGeneration
32-
// AppliedGeneration
33-
// LastObservedAt
34-
// LastAppliedAt
35-
// ProcessingDigest
36-
// ProcessingSince
37-
Revision: c.release.Revision,
38-
// Conditions
39-
State: c.release.State,
40-
Inventory: c.release.Inventory,
41-
}
43+
return &c.Status.Status
4244
}
4345

4446
func componentFromRelease(release *release.Release, values map[string]any) *Component {
@@ -54,7 +56,29 @@ func componentFromRelease(release *release.Release, values map[string]any) *Comp
5456
Name: release.GetName(),
5557
},
5658
},
57-
release: release,
58-
values: values,
59+
Spec: ComponentSpec{
60+
Values: &apiextensionsv1.JSON{
61+
Raw: must(json.Marshal(values)),
62+
},
63+
},
64+
Status: ComponentStatus{
65+
Status: component.Status{
66+
// TODO: populate missing fields
67+
// ObservedGeneration
68+
// AppliedGeneration
69+
// LastObservedAt
70+
// LastAppliedAt
71+
ProcessingDigest: release.GetDigest(),
72+
// ProcessingSince
73+
// LastProcessingDigest
74+
Revision: release.Revision,
75+
// Conditions
76+
State: release.State,
77+
Inventory: release.Inventory,
78+
},
79+
LastAttemptedDigest: release.GetDigest(),
80+
LastAttemptedRevision: release.GetDigest(),
81+
},
82+
values: values,
5983
}
6084
}

clm/internal/manifests/generate.go

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -47,39 +47,45 @@ func Generate(manifestSources []string, valuesSources []string, reconcilerName s
4747

4848
for _, source := range manifestSources {
4949
// TODO: support helm, oci URLs
50-
path := source
5150

52-
if info, err := os.Stat(path); err != nil {
51+
var fsys fs.FS
52+
var path string
53+
54+
source, err := filepath.Abs(source)
55+
if err != nil {
56+
return nil, err
57+
}
58+
59+
if info, err := os.Stat(source); err != nil {
5360
if os.IsNotExist(err) {
54-
return nil, fmt.Errorf("no such file or directory: %s", path)
61+
return nil, fmt.Errorf("no such file or directory: %s", source)
5562
} else {
5663
return nil, err
5764
}
58-
} else if !info.IsDir() {
65+
} else if info.IsDir() {
66+
fsys = os.DirFS("/")
67+
path = source[1:]
68+
} else {
5969
tmpdir, err := os.MkdirTemp("", "clm-")
6070
if err != nil {
6171
return nil, err
6272
}
6373
defer os.RemoveAll(tmpdir)
64-
if _, err := copyFile(path, fmt.Sprintf("%s/%s", tmpdir, "resources.yaml")); err != nil {
74+
if _, err := copyFile(source, fmt.Sprintf("%s/%s", tmpdir, "resources.yaml")); err != nil {
6575
return nil, err
6676
}
67-
path = tmpdir
77+
fsys = os.DirFS(tmpdir)
78+
path = ""
6879
}
69-
path, err := filepath.Abs(path)
70-
if err != nil {
71-
return nil, err
72-
}
73-
fsys := os.DirFS(path)
7480

7581
var generator manifests.Generator
76-
if _, err = fs.Stat(fsys, "Chart.yaml"); err == nil {
77-
generator, err = helm.NewHelmGenerator(fsys, "", nil)
82+
if _, err = fs.Stat(fsys, filepath.Clean(path+"/Chart.yaml")); err == nil {
83+
generator, err = helm.NewHelmGenerator(fsys, path, nil)
7884
if err != nil {
7985
return nil, err
8086
}
8187
} else if errors.Is(err, fs.ErrNotExist) {
82-
generator, err = kustomize.NewKustomizeGenerator(fsys, "", nil, kustomize.KustomizeGeneratorOptions{})
88+
generator, err = kustomize.NewKustomizeGenerator(fsys, path, nil, kustomize.KustomizeGeneratorOptions{})
8389
if err != nil {
8490
return nil, err
8591
}
@@ -88,17 +94,17 @@ func Generate(manifestSources []string, valuesSources []string, reconcilerName s
8894
}
8995

9096
releaseComponent := componentFromRelease(release, allValues)
91-
// TODO: what about component digest
97+
9298
generateCtx := component.NewContext(context.TODO()).
9399
WithReconcilerName(reconcilerName).
94100
WithLocalClient(clnt).
95101
WithClient(clnt).
96102
WithComponent(releaseComponent).
97-
WithComponentName(releaseComponent.GetName()).
98-
WithComponentNamespace(releaseComponent.GetNamespace()).
99-
WithComponentDigest("").
100-
WithComponentRevision(releaseComponent.GetStatus().Revision)
101-
objects, err := generator.Generate(generateCtx, release.GetNamespace(), release.GetName(), types.UnstructurableMap(allValues))
103+
WithComponentName(releaseComponent.Name).
104+
WithComponentNamespace(releaseComponent.Namespace).
105+
WithComponentDigest(releaseComponent.Status.ProcessingDigest).
106+
WithComponentRevision(releaseComponent.Status.Revision)
107+
objects, err := generator.Generate(generateCtx, releaseComponent.Namespace, releaseComponent.Name, types.UnstructurableMap(allValues))
102108
if err != nil {
103109
return nil, err
104110
}

clm/internal/manifests/util.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ import (
1111
"os"
1212
)
1313

14+
// TODO: consolidate all the util files into an internal reuse package
15+
16+
func must[T any](x T, err error) T {
17+
if err != nil {
18+
panic(err)
19+
}
20+
return x
21+
}
22+
1423
func copyFile(src, dst string) (int64, error) {
1524
sourceFileStat, err := os.Stat(src)
1625
if err != nil {

clm/internal/release/release.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ SPDX-License-Identifier: Apache-2.0
66
package release
77

88
import (
9+
"fmt"
910
"strconv"
1011
"time"
1112

@@ -36,6 +37,13 @@ type Release struct {
3637
State component.State
3738
}
3839

40+
func NewRelease(namespace string, name string) *Release {
41+
return &Release{
42+
namespace: namespace,
43+
name: name,
44+
}
45+
}
46+
3947
func (r *Release) GetNamespace() string {
4048
return r.namespace
4149
}
@@ -44,6 +52,10 @@ func (r *Release) GetName() string {
4452
return r.name
4553
}
4654

55+
func (r *Release) GetDigest() string {
56+
return sha256hex([]byte(fmt.Sprintf("%d", r.Revision)))
57+
}
58+
4759
func (r *Release) IsDeleting() bool {
4860
return !r.configMap.DeletionTimestamp.IsZero()
4961
}

clm/internal/release/util.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and component-operator-runtime contributors
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package release
7+
8+
import (
9+
"crypto/sha256"
10+
"encoding/hex"
11+
)
12+
13+
// TODO: consolidate all the util files into an internal reuse package
14+
15+
func sha256hex(data []byte) string {
16+
sum := sha256.Sum256(data)
17+
return hex.EncodeToString(sum[:])
18+
}

0 commit comments

Comments
 (0)