@@ -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().
6464type 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
7576var _ 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.
197233func (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.
206248type 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+
411519func init () {
412520 SchemeBuilder .Register (& Component {}, & ComponentList {})
521+ SchemeBuilder .Register (& Blueprint {}, & BlueprintList {})
522+ SchemeBuilder .Register (& BlueprintVersion {}, & BlueprintVersionList {})
413523}
0 commit comments