Skip to content

Commit 62fe54a

Browse files
authored
Merge pull request #93 from SAP/blueprints
add blueprints source
2 parents d77eaaf + f867681 commit 62fe54a

43 files changed

Lines changed: 2236 additions & 650 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ The mandatory field `spec.sourceRef` defines the source of the manifests used fo
9292
Currently, the following types of sources are supported (exactly one must be present):
9393

9494
```yaml
95+
# In-cluster blueprint
96+
sourceRef:
97+
blueprint:
98+
# namespace: source-ns
99+
name: blueprint-name
100+
95101
# Flux GitRepository
96102
sourceRef:
97103
fluxGitRepository:
@@ -117,7 +123,26 @@ sourceRef:
117123
name: helmchart-name
118124
```
119125

120-
Cross-namespace references are allowed; if namespace is not provided, the source will be assumed to exist in the component's namespace.
126+
Cross-namespace references are allowed; if namespace is not provided, the source will be assumed to exist in the component's namespace. Unlike the flux based source types, blueprints serve as an in-cluster representation holding the manifests of the dependent resources. A blueprint could look like this:
127+
128+
```yaml
129+
---
130+
apiVersion: core.cs.sap.com/v1alpha1
131+
kind: Blueprint
132+
metadata:
133+
namespace: source-ns
134+
name: blueprint-name
135+
spec:
136+
files:
137+
resources/cm.yaml: |-
138+
---
139+
apiVersion: v1
140+
kind: ConfigMap
141+
metadata:
142+
name: {{ name }}-cm
143+
data:
144+
foo: bar
145+
```
121146

122147
### Source revision and digest
123148

api/v1alpha1/types.go

Lines changed: 137 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
apierrors "k8s.io/apimachinery/pkg/api/errors"
1515
apimeta "k8s.io/apimachinery/pkg/api/meta"
1616
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17-
runtime "k8s.io/apimachinery/pkg/runtime"
17+
"k8s.io/apimachinery/pkg/runtime"
1818
apitypes "k8s.io/apimachinery/pkg/types"
1919
"sigs.k8s.io/controller-runtime/pkg/client"
2020

@@ -26,9 +26,9 @@ import (
2626
"github.com/sap/component-operator-runtime/pkg/manifests"
2727
componentoperatorruntimetypes "github.com/sap/component-operator-runtime/pkg/types"
2828

29+
httprepositoryutil "github.com/sap/component-operator/internal/httprepository/util"
2930
"github.com/sap/component-operator/internal/object"
30-
flux "github.com/sap/component-operator/internal/sources/flux/types"
31-
httprepository "github.com/sap/component-operator/internal/sources/httprepository/util"
31+
"github.com/sap/component-operator/pkg/meta"
3232
)
3333

3434
// ComponentSpec defines the desired state of Component.
@@ -56,20 +56,21 @@ type ComponentSpec struct {
5656
Dependencies []Dependency `json:"dependencies,omitempty"`
5757
}
5858

59-
// +kubebuilder:validation:XValidation:rule="has(self.httpRepository) && !has(self.fluxGitRepository) && !has(self.fluxOciRepository) && !has(self.fluxBucket) && !has(self.fluxHelmChart) || !has(self.httpRepository) && has(self.fluxGitRepository) && !has(self.fluxOciRepository) && !has(self.fluxBucket) && !has(self.fluxHelmChart) || !has(self.httpRepository) && !has(self.fluxGitRepository) && has(self.fluxOciRepository) && !has(self.fluxBucket) && !has(self.fluxHelmChart) || !has(self.httpRepository) && !has(self.fluxGitRepository) && !has(self.fluxOciRepository) && has(self.fluxBucket) && !has(self.fluxHelmChart) || !has(self.httpRepository) && !has(self.fluxGitRepository) && !has(self.fluxOciRepository) && !has(self.fluxBucket) && has(self.fluxHelmChart)",message="Exactly one of 'httpRepository' or 'fluxGitRepository' or 'fluxOciRepository' or 'fluxBucket' or 'fluxHelmChart' must be provided"
59+
// +kubebuilder:validation:XValidation:rule="has(self.blueprint) && !has(self.httpRepository) && !has(self.fluxGitRepository) && !has(self.fluxOciRepository) && !has(self.fluxBucket) && !has(self.fluxHelmChart) || !has(self.blueprint) && has(self.httpRepository) && !has(self.fluxGitRepository) && !has(self.fluxOciRepository) && !has(self.fluxBucket) && !has(self.fluxHelmChart) || !has(self.blueprint) && !has(self.httpRepository) && has(self.fluxGitRepository) && !has(self.fluxOciRepository) && !has(self.fluxBucket) && !has(self.fluxHelmChart) || !has(self.blueprint) && !has(self.httpRepository) && !has(self.fluxGitRepository) && has(self.fluxOciRepository) && !has(self.fluxBucket) && !has(self.fluxHelmChart) || !has(self.blueprint) && !has(self.httpRepository) && !has(self.fluxGitRepository) && !has(self.fluxOciRepository) && has(self.fluxBucket) && !has(self.fluxHelmChart) || !has(self.blueprint) && !has(self.httpRepository) && !has(self.fluxGitRepository) && !has(self.fluxOciRepository) && !has(self.fluxBucket) && has(self.fluxHelmChart)",message="Exactly one of 'blueprint' or 'httpRepository' or 'fluxGitRepository' or 'fluxOciRepository' or 'fluxBucket' or 'fluxHelmChart' must be provided"
6060

6161
// SourceReference models the source of the templates used to render the dependent resources.
6262
// Exactly one of the options must be provided. Before accessing the Artifact() method,
6363
// the SourceReference must be loaded by calling Load().
6464
type SourceReference struct {
65-
HttpRepository *HttpRepository `json:"httpRepository,omitempty"`
66-
FluxGitRepository *FluxGitRepository `json:"fluxGitRepository,omitempty"`
67-
FluxOciRepository *FluxOciRepository `json:"fluxOciRepository,omitempty"`
68-
FluxBucket *FluxBucket `json:"fluxBucket,omitempty"`
69-
FluxHelmChart *FluxHelmChart `json:"fluxHelmChart,omitempty"`
70-
artifact Artifact `json:"-"`
71-
digest string `json:"-"`
72-
loaded bool `json:"-"`
65+
Blueprint *BlueprintReference `json:"blueprint,omitempty"`
66+
HttpRepository *HttpRepository `json:"httpRepository,omitempty"`
67+
FluxGitRepository *FluxGitRepositoryReference `json:"fluxGitRepository,omitempty"`
68+
FluxOciRepository *FluxOciRepositoryReference `json:"fluxOciRepository,omitempty"`
69+
FluxBucket *FluxBucketReference `json:"fluxBucket,omitempty"`
70+
FluxHelmChart *FluxHelmChartReference `json:"fluxHelmChart,omitempty"`
71+
artifact Artifact `json:"-"`
72+
digest string `json:"-"`
73+
loaded bool `json:"-"`
7374
}
7475

7576
var _ component.Reference[*Component] = &SourceReference{}
@@ -98,8 +99,43 @@ func (r *SourceReference) Load(ctx context.Context, clnt client.Client, componen
9899
var digestData []any
99100

100101
switch {
102+
case sourceRef.Blueprint != nil:
103+
blueprint := &Blueprint{}
104+
if err := clnt.Get(ctx, apitypes.NamespacedName(sourceRef.Blueprint.WithDefaultNamespace(component.Namespace)), blueprint); err != nil {
105+
if apierrors.IsNotFound(err) {
106+
return componentoperatorruntimetypes.NewRetriableError(err, new(10*time.Second))
107+
}
108+
return err
109+
}
110+
111+
blueprintDigest := blueprint.GetDigest()
112+
blueprintRevision := blueprint.GetRevision()
113+
blueprintVersion := &BlueprintVersion{
114+
TypeMeta: metav1.TypeMeta{
115+
Kind: KindBlueprintVersion,
116+
APIVersion: GroupVersion.String(),
117+
},
118+
ObjectMeta: metav1.ObjectMeta{
119+
Name: fmt.Sprintf("%s--%s", blueprint.Name, blueprintDigest),
120+
Namespace: blueprint.Namespace,
121+
},
122+
Spec: BlueprintVersionSpec{
123+
Blueprint: blueprint.Name,
124+
Digest: blueprintDigest,
125+
Revision: blueprintRevision,
126+
BlueprintSpec: blueprint.Spec,
127+
},
128+
}
129+
if err := clnt.Patch(ctx, blueprintVersion, client.Apply, client.FieldOwner(meta.Name), client.ForceOwnership); err != nil {
130+
return err
131+
}
132+
133+
sourceRefArtifact.Url = fmt.Sprintf("blueprint://%s/%s/%s", blueprint.Namespace, blueprint.Name, blueprintDigest)
134+
sourceRefArtifact.Digest = blueprintDigest
135+
sourceRefArtifact.Revision = blueprintRevision
136+
digestData = []any{sourceRefArtifact.Url, sourceRefArtifact.Digest, sourceRefArtifact.Revision}
101137
case sourceRef.HttpRepository != nil:
102-
url, digest, revision, err := httprepository.GetArtifact(sourceRef.HttpRepository.Url, sourceRef.HttpRepository.DigestHeader, sourceRef.HttpRepository.RevisionHeader)
138+
url, digest, revision, err := httprepositoryutil.GetArtifact(sourceRef.HttpRepository.Url, sourceRef.HttpRepository.DigestHeader, sourceRef.HttpRepository.RevisionHeader)
103139
if err != nil {
104140
return err
105141
}
@@ -110,7 +146,7 @@ func (r *SourceReference) Load(ctx context.Context, clnt client.Client, componen
110146
digestData = []any{sourceRefArtifact.Url, sourceRefArtifact.Digest, sourceRefArtifact.Revision}
111147
case sourceRef.FluxGitRepository != nil, sourceRef.FluxOciRepository != nil, sourceRef.FluxBucket != nil, sourceRef.FluxHelmChart != nil:
112148
var sourceName NamespacedName
113-
var source flux.Source
149+
var source meta.FluxSource
114150

115151
switch {
116152
case sourceRef.FluxGitRepository != nil:
@@ -131,27 +167,27 @@ func (r *SourceReference) Load(ctx context.Context, clnt client.Client, componen
131167

132168
if err := clnt.Get(ctx, apitypes.NamespacedName(sourceName), source); err != nil {
133169
if apimeta.IsNoMatchError(err) || apierrors.IsNotFound(err) {
134-
return componentoperatorruntimetypes.NewRetriableError(err, ref(10*time.Second))
170+
return componentoperatorruntimetypes.NewRetriableError(err, new(10*time.Second))
135171
}
136172
return err
137173
}
138174
if !object.IsReady(source) {
139-
return componentoperatorruntimetypes.NewRetriableError(fmt.Errorf("source not ready"), ref(10*time.Second))
175+
return componentoperatorruntimetypes.NewRetriableError(fmt.Errorf("source not ready"), new(10*time.Second))
140176
}
141177

142178
artifact := source.GetArtifact()
143179
if artifact == nil {
144-
return componentoperatorruntimetypes.NewRetriableError(fmt.Errorf("missing artifact on ready source"), ref(10*time.Second))
180+
return componentoperatorruntimetypes.NewRetriableError(fmt.Errorf("missing artifact on ready source"), new(10*time.Second))
145181
}
146182

147183
if artifact.URL == "" {
148-
return componentoperatorruntimetypes.NewRetriableError(fmt.Errorf("source not ready (missing URL)"), ref(10*time.Second))
184+
return componentoperatorruntimetypes.NewRetriableError(fmt.Errorf("source not ready (missing URL)"), new(10*time.Second))
149185
}
150186
if artifact.Digest == "" {
151-
return componentoperatorruntimetypes.NewRetriableError(fmt.Errorf("source not ready (missing digest)"), ref(10*time.Second))
187+
return componentoperatorruntimetypes.NewRetriableError(fmt.Errorf("source not ready (missing digest)"), new(10*time.Second))
152188
}
153189
if artifact.Revision == "" {
154-
return componentoperatorruntimetypes.NewRetriableError(fmt.Errorf("source not ready (missing revision)"), ref(10*time.Second))
190+
return componentoperatorruntimetypes.NewRetriableError(fmt.Errorf("source not ready (missing revision)"), new(10*time.Second))
155191
}
156192

157193
sourceRefArtifact.Url = artifact.URL
@@ -195,13 +231,19 @@ func (r *SourceReference) Artifact() Artifact {
195231

196232
// Check if source reference equals other given source reference.
197233
func (r *SourceReference) Equals(s *SourceReference) bool {
198-
return equal(r.HttpRepository, s.HttpRepository) &&
234+
return equal(r.Blueprint, s.Blueprint) &&
235+
equal(r.HttpRepository, s.HttpRepository) &&
199236
equal(r.FluxGitRepository, s.FluxGitRepository) &&
200237
equal(r.FluxOciRepository, s.FluxOciRepository) &&
201238
equal(r.FluxBucket, s.FluxBucket) &&
202239
equal(r.FluxHelmChart, s.FluxHelmChart)
203240
}
204241

242+
// Reference to a Blueprint.
243+
type BlueprintReference struct {
244+
NamespacedName `json:",inline"`
245+
}
246+
205247
// Reference to a generic http repository.
206248
type HttpRepository struct {
207249
// URL of the source. Authentication is currently not supported. The operator will make HEAD requests to retrieve the digest/revision
@@ -217,22 +259,22 @@ type HttpRepository struct {
217259
}
218260

219261
// Reference to a flux GitRepository.
220-
type FluxGitRepository struct {
262+
type FluxGitRepositoryReference struct {
221263
NamespacedName `json:",inline"`
222264
}
223265

224266
// Reference to a flux OCIRepository.
225-
type FluxOciRepository struct {
267+
type FluxOciRepositoryReference struct {
226268
NamespacedName `json:",inline"`
227269
}
228270

229271
// Reference to a flux Bucket.
230-
type FluxBucket struct {
272+
type FluxBucketReference struct {
231273
NamespacedName `json:",inline"`
232274
}
233275

234276
// Reference to a flux HelmChart.
235-
type FluxHelmChart struct {
277+
type FluxHelmChartReference struct {
236278
NamespacedName `json:",inline"`
237279
}
238280

@@ -404,10 +446,78 @@ func isComponentProcessing(c *Component) bool {
404446
return c.Status.ProcessingSince != nil && c.Status.LastObservedAt.Sub(c.Status.ProcessingSince.Time) < timeout
405447
}
406448

407-
func equal[T comparable](x *T, y *T) bool {
408-
return x == nil && y == nil || x != nil && y != nil && *x == *y
449+
// BlueprintSpec defines the desired state of Blueprint.
450+
type BlueprintSpec struct {
451+
Files map[string]string `json:"files,omitempty"`
452+
}
453+
454+
// +kubebuilder:object:root=true
455+
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
456+
// +genclient
457+
458+
// Blueprint is the Schema for the blueprints API.
459+
type Blueprint struct {
460+
metav1.TypeMeta `json:",inline"`
461+
metav1.ObjectMeta `json:"metadata,omitempty"`
462+
463+
Spec BlueprintSpec `json:"spec"`
464+
}
465+
466+
func (b *Blueprint) GetDigest() string {
467+
return calculateDigest(b.Spec)
468+
}
469+
470+
func (b *Blueprint) GetRevision() string {
471+
return fmt.Sprintf("generation:%d", b.Generation)
409472
}
410473

474+
// +kubebuilder:object:root=true
475+
476+
// BlueprintList contains a list of Blueprint.
477+
type BlueprintList struct {
478+
metav1.TypeMeta `json:",inline"`
479+
metav1.ListMeta `json:"metadata,omitempty"`
480+
Items []Blueprint `json:"items"`
481+
}
482+
483+
// BlueprintVersionSpec defines the desired state of BlueprintVersion.
484+
type BlueprintVersionSpec struct {
485+
Blueprint string `json:"blueprint"`
486+
Digest string `json:"digest"`
487+
Revision string `json:"revision"`
488+
BlueprintSpec `json:",inline"`
489+
}
490+
491+
// +kubebuilder:object:root=true
492+
// +kubebuilder:selectablefield:JSONPath=".spec.blueprint"
493+
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
494+
// +genclient
495+
496+
// BlueprintVersion is the Schema for the blueprint versions API.
497+
type BlueprintVersion struct {
498+
metav1.TypeMeta `json:",inline"`
499+
metav1.ObjectMeta `json:"metadata,omitempty"`
500+
501+
Spec BlueprintVersionSpec `json:"spec"`
502+
}
503+
504+
// +kubebuilder:object:root=true
505+
506+
// BlueprintVersionList contains a list of BlueprintVersion.
507+
type BlueprintVersionList struct {
508+
metav1.TypeMeta `json:",inline"`
509+
metav1.ListMeta `json:"metadata,omitempty"`
510+
Items []BlueprintVersion `json:"items"`
511+
}
512+
513+
const (
514+
KindComponent = "Component"
515+
KindBlueprint = "Blueprint"
516+
KindBlueprintVersion = "BlueprintVersion"
517+
)
518+
411519
func init() {
412520
SchemeBuilder.Register(&Component{}, &ComponentList{})
521+
SchemeBuilder.Register(&Blueprint{}, &BlueprintList{})
522+
SchemeBuilder.Register(&BlueprintVersion{}, &BlueprintVersionList{})
413523
}

api/v1alpha1/util.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import (
1313

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

16-
func ref[T any](x T) *T {
17-
return &x
16+
func equal[T comparable](x *T, y *T) bool {
17+
return x == nil && y == nil || x != nil && y != nil && *x == *y
1818
}
1919

2020
func sha256hex(data []byte) string {

0 commit comments

Comments
 (0)