From 9a2b3f68f9438e27f55f101448822c13fd51db29 Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Mon, 29 Jun 2026 13:28:51 +0300 Subject: [PATCH 01/31] feat(dhei-20905): api and gen for dds --- common/pkg/config/feature.go | 5 + rover-server/api/openapi.yaml | 58 ++++ rover-server/internal/api/server.gen.go | 419 +++++++++++++++--------- 3 files changed, 335 insertions(+), 147 deletions(-) diff --git a/common/pkg/config/feature.go b/common/pkg/config/feature.go index 90254bac7..0418e5e85 100644 --- a/common/pkg/config/feature.go +++ b/common/pkg/config/feature.go @@ -48,6 +48,11 @@ var ( FeaturePermission Feature = NewFeature("permission", false) // Permission feature disabled by default FeatureSecretManager Feature = NewFeature("secret_manager", true) // Secret Manager feature enabled by default FeatureFileManager Feature = NewFeature("file_manager", true) // File Manager feature enabled by default + // TODO(DHEI-20905): File/DDS domain feature disabled by default. The rover-domain + // dispatch is in place, but the File domain (DHEI-20903) and SFTP/DDS domain + // (DHEI-20904) are not yet available. Enable this flag once those domains land so + // the rover operator can create file-domain resources instead of returning Blocked. + FeatureFile Feature = NewFeature("file", false) ) // SetFeatureEnabled sets the enabled state for a feature. Intended for tests. diff --git a/rover-server/api/openapi.yaml b/rover-server/api/openapi.yaml index 0ea7132fb..dff78ebee 100644 --- a/rover-server/api/openapi.yaml +++ b/rover-server/api/openapi.yaml @@ -1739,6 +1739,40 @@ components: type: array items: $ref: '#/components/schemas/EventScope' + FileExposure: + type: object + required: + - type + - fileType + - publicKeys + properties: + type: + type: string + fileType: + type: string + variant: + type: string + enum: + - dds + description: File-transfer backend. Optional; currently only "dds" is supported. + visibility: + $ref: '#/components/schemas/Visibility' + publicKeys: + type: array + items: + $ref: '#/components/schemas/PublicKey' + PublicKey: + type: object + required: + - label + - key + properties: + label: + type: string + description: Human-readable identifier for the key. Must be unique per fileType. + key: + type: string + description: SSH public key value. Must be unique per fileType. Exposure: type: object discriminator: @@ -1746,9 +1780,11 @@ components: mapping: api: '#/components/schemas/ApiExposure' event: '#/components/schemas/EventExposure' + file: '#/components/schemas/FileExposure' oneOf: - $ref: '#/components/schemas/ApiExposure' - $ref: '#/components/schemas/EventExposure' + - $ref: '#/components/schemas/FileExposure' ApiExposureInfo: @@ -2409,6 +2445,26 @@ components: description: > If true, the health check will be performed with a GET request instead of a HEAD request + FileSubscription: + type: object + required: + - type + - fileType + - publicKeys + properties: + type: + type: string + fileType: + type: string + variant: + type: string + enum: + - dds + description: File-transfer backend. Optional; currently only "dds" is supported. + publicKeys: + type: array + items: + $ref: '#/components/schemas/PublicKey' Subscription: type: object discriminator: @@ -2416,9 +2472,11 @@ components: mapping: api: '#/components/schemas/ApiSubscription' event: '#/components/schemas/EventSubscription' + file: '#/components/schemas/FileSubscription' oneOf: - $ref: '#/components/schemas/ApiSubscription' - $ref: '#/components/schemas/EventSubscription' + - $ref: '#/components/schemas/FileSubscription' ApiSubscriptionInfo: type: object diff --git a/rover-server/internal/api/server.gen.go b/rover-server/internal/api/server.gen.go index d51419e7a..b52f49d13 100644 --- a/rover-server/internal/api/server.gen.go +++ b/rover-server/internal/api/server.gen.go @@ -1,7 +1,3 @@ -// Copyright 2026 Deutsche Telekom IT GmbH -// -// SPDX-License-Identifier: Apache-2.0 - // Package api provides primitives to interact with the openapi HTTP API. // // Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.6.1-0.20260425061340-097bc33cf46c DO NOT EDIT. @@ -162,6 +158,36 @@ func (e EventTriggerResponseFilterMode) Valid() bool { } } +// Defines values for FileExposureVariant. +const ( + FileExposureVariantDds FileExposureVariant = "dds" +) + +// Valid indicates whether the value is a known member of the FileExposureVariant enum. +func (e FileExposureVariant) Valid() bool { + switch e { + case FileExposureVariantDds: + return true + default: + return false + } +} + +// Defines values for FileSubscriptionVariant. +const ( + FileSubscriptionVariantDds FileSubscriptionVariant = "dds" +) + +// Valid indicates whether the value is a known member of the FileSubscriptionVariant enum. +func (e FileSubscriptionVariant) Valid() bool { + switch e { + case FileSubscriptionVariantDds: + return true + default: + return false + } +} + // Defines values for GrantType. const ( CLIENTCREDENTIALS GrantType = "CLIENT_CREDENTIALS" @@ -949,6 +975,33 @@ type FieldProblem struct { Title string `json:"title"` } +// FileExposure defines model for FileExposure. +type FileExposure struct { + FileType string `json:"fileType"` + PublicKeys []PublicKey `json:"publicKeys"` + Type string `json:"type"` + + // Variant File-transfer backend. Optional; currently only "dds" is supported. + Variant FileExposureVariant `json:"variant,omitempty,omitzero"` + Visibility Visibility `json:"visibility,omitempty,omitzero"` +} + +// FileExposureVariant File-transfer backend. Optional; currently only "dds" is supported. +type FileExposureVariant string + +// FileSubscription defines model for FileSubscription. +type FileSubscription struct { + FileType string `json:"fileType"` + PublicKeys []PublicKey `json:"publicKeys"` + Type string `json:"type"` + + // Variant File-transfer backend. Optional; currently only "dds" is supported. + Variant FileSubscriptionVariant `json:"variant,omitempty,omitzero"` +} + +// FileSubscriptionVariant File-transfer backend. Optional; currently only "dds" is supported. +type FileSubscriptionVariant string + // GrantType defines model for GrantType. type GrantType string @@ -1017,6 +1070,15 @@ type Problem struct { // ProcessingState defines model for ProcessingState. type ProcessingState string +// PublicKey defines model for PublicKey. +type PublicKey struct { + // Key SSH public key value. Must be unique per fileType. + Key string `json:"key"` + + // Label Human-readable identifier for the key. Must be unique per fileType. + Label string `json:"label"` +} + // RateLimit defines model for RateLimit. type RateLimit struct { FaultTolerant bool `json:"faultTolerant,omitempty,omitzero"` @@ -1467,6 +1529,34 @@ func (t *Exposure) MergeEventExposure(v EventExposure) error { return err } +// AsFileExposure returns the union data inside the Exposure as a FileExposure +func (t Exposure) AsFileExposure() (FileExposure, error) { + var body FileExposure + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromFileExposure overwrites any union data inside the Exposure as the provided FileExposure +func (t *Exposure) FromFileExposure(v FileExposure) error { + v.Type = "file" + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeFileExposure performs a merge with any union data inside the Exposure, using the provided FileExposure +func (t *Exposure) MergeFileExposure(v FileExposure) error { + v.Type = "file" + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + func (t Exposure) Discriminator() (string, error) { var discriminator struct { Discriminator string `json:"type"` @@ -1485,6 +1575,8 @@ func (t Exposure) ValueByDiscriminator() (interface{}, error) { return t.AsApiExposure() case "event": return t.AsEventExposure() + case "file": + return t.AsFileExposure() default: return nil, errors.New("unknown discriminator value: " + discriminator) } @@ -1734,6 +1826,34 @@ func (t *Subscription) MergeEventSubscription(v EventSubscription) error { return err } +// AsFileSubscription returns the union data inside the Subscription as a FileSubscription +func (t Subscription) AsFileSubscription() (FileSubscription, error) { + var body FileSubscription + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromFileSubscription overwrites any union data inside the Subscription as the provided FileSubscription +func (t *Subscription) FromFileSubscription(v FileSubscription) error { + v.Type = "file" + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeFileSubscription performs a merge with any union data inside the Subscription, using the provided FileSubscription +func (t *Subscription) MergeFileSubscription(v FileSubscription) error { + v.Type = "file" + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + func (t Subscription) Discriminator() (string, error) { var discriminator struct { Discriminator string `json:"type"` @@ -1752,6 +1872,8 @@ func (t Subscription) ValueByDiscriminator() (interface{}, error) { return t.AsApiSubscription() case "event": return t.AsEventSubscription() + case "file": + return t.AsFileSubscription() default: return nil, errors.New("unknown discriminator value: " + discriminator) } @@ -1859,149 +1981,152 @@ func (t *SubscriptionInfo) UnmarshalJSON(b []byte) error { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "+/8DAAD//7AbpLxrCgEA", + "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", + "yNrth9v/DQAA//+C2Rh/Ng8BAA==", } // GetSwagger returns the content of the embedded swagger specification file From 9cc74678a4a39b97c486419ecdaaf66b68fc5d54 Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Tue, 30 Jun 2026 10:05:54 +0300 Subject: [PATCH 02/31] feat(dhei-20905): add info for exposure and subs --- rover-server/api/openapi.yaml | 22 ++ rover-server/internal/api/server.gen.go | 437 ++++++++++++++++-------- 2 files changed, 317 insertions(+), 142 deletions(-) diff --git a/rover-server/api/openapi.yaml b/rover-server/api/openapi.yaml index dff78ebee..2c21455ed 100644 --- a/rover-server/api/openapi.yaml +++ b/rover-server/api/openapi.yaml @@ -1805,15 +1805,26 @@ components: type: string enum: - event + FileExposureInfo: + type: object + allOf: + - $ref: '#/components/schemas/FileExposure' + - properties: + type: + type: string + enum: + - file ExposureInfo: discriminator: propertyName: type mapping: api: '#/components/schemas/ApiExposureInfo' event: '#/components/schemas/EventExposureInfo' + file: '#/components/schemas/FileExposureInfo' oneOf: - $ref: '#/components/schemas/ApiExposureInfo' - $ref: '#/components/schemas/EventExposureInfo' + - $ref: '#/components/schemas/FileExposureInfo' ApplicationInfo: type: object @@ -2504,6 +2515,15 @@ components: type: string horizonSubscriptionUrl: type: string + FileSubscriptionInfo: + type: object + allOf: + - $ref: '#/components/schemas/FileSubscription' + - properties: + type: + type: string + enum: + - file SubscriptionInfo: readOnly: true discriminator: @@ -2511,9 +2531,11 @@ components: mapping: api: '#/components/schemas/ApiSubscriptionInfo' event: '#/components/schemas/EventSubscriptionInfo' + file: '#/components/schemas/FileSubscriptionInfo' oneOf: - $ref: '#/components/schemas/ApiSubscriptionInfo' - $ref: '#/components/schemas/EventSubscriptionInfo' + - $ref: '#/components/schemas/FileSubscriptionInfo' EventTrigger: type: object diff --git a/rover-server/internal/api/server.gen.go b/rover-server/internal/api/server.gen.go index b52f49d13..0a3624d22 100644 --- a/rover-server/internal/api/server.gen.go +++ b/rover-server/internal/api/server.gen.go @@ -173,6 +173,36 @@ func (e FileExposureVariant) Valid() bool { } } +// Defines values for FileExposureInfoType. +const ( + FileExposureInfoTypeFile FileExposureInfoType = "file" +) + +// Valid indicates whether the value is a known member of the FileExposureInfoType enum. +func (e FileExposureInfoType) Valid() bool { + switch e { + case FileExposureInfoTypeFile: + return true + default: + return false + } +} + +// Defines values for FileExposureInfoVariant. +const ( + FileExposureInfoVariantDds FileExposureInfoVariant = "dds" +) + +// Valid indicates whether the value is a known member of the FileExposureInfoVariant enum. +func (e FileExposureInfoVariant) Valid() bool { + switch e { + case FileExposureInfoVariantDds: + return true + default: + return false + } +} + // Defines values for FileSubscriptionVariant. const ( FileSubscriptionVariantDds FileSubscriptionVariant = "dds" @@ -188,6 +218,36 @@ func (e FileSubscriptionVariant) Valid() bool { } } +// Defines values for FileSubscriptionInfoType. +const ( + FileSubscriptionInfoTypeFile FileSubscriptionInfoType = "file" +) + +// Valid indicates whether the value is a known member of the FileSubscriptionInfoType enum. +func (e FileSubscriptionInfoType) Valid() bool { + switch e { + case FileSubscriptionInfoTypeFile: + return true + default: + return false + } +} + +// Defines values for FileSubscriptionInfoVariant. +const ( + FileSubscriptionInfoVariantDds FileSubscriptionInfoVariant = "dds" +) + +// Valid indicates whether the value is a known member of the FileSubscriptionInfoVariant enum. +func (e FileSubscriptionInfoVariant) Valid() bool { + switch e { + case FileSubscriptionInfoVariantDds: + return true + default: + return false + } +} + // Defines values for GrantType. const ( CLIENTCREDENTIALS GrantType = "CLIENT_CREDENTIALS" @@ -989,6 +1049,23 @@ type FileExposure struct { // FileExposureVariant File-transfer backend. Optional; currently only "dds" is supported. type FileExposureVariant string +// FileExposureInfo defines model for FileExposureInfo. +type FileExposureInfo struct { + FileType string `json:"fileType"` + PublicKeys []PublicKey `json:"publicKeys"` + Type FileExposureInfoType `json:"type"` + + // Variant File-transfer backend. Optional; currently only "dds" is supported. + Variant FileExposureInfoVariant `json:"variant,omitempty,omitzero"` + Visibility Visibility `json:"visibility,omitempty,omitzero"` +} + +// FileExposureInfoType defines model for FileExposureInfo.Type. +type FileExposureInfoType string + +// FileExposureInfoVariant File-transfer backend. Optional; currently only "dds" is supported. +type FileExposureInfoVariant string + // FileSubscription defines model for FileSubscription. type FileSubscription struct { FileType string `json:"fileType"` @@ -1002,6 +1079,22 @@ type FileSubscription struct { // FileSubscriptionVariant File-transfer backend. Optional; currently only "dds" is supported. type FileSubscriptionVariant string +// FileSubscriptionInfo defines model for FileSubscriptionInfo. +type FileSubscriptionInfo struct { + FileType string `json:"fileType"` + PublicKeys []PublicKey `json:"publicKeys"` + Type FileSubscriptionInfoType `json:"type"` + + // Variant File-transfer backend. Optional; currently only "dds" is supported. + Variant FileSubscriptionInfoVariant `json:"variant,omitempty,omitzero"` +} + +// FileSubscriptionInfoType defines model for FileSubscriptionInfo.Type. +type FileSubscriptionInfoType string + +// FileSubscriptionInfoVariant File-transfer backend. Optional; currently only "dds" is supported. +type FileSubscriptionInfoVariant string + // GrantType defines model for GrantType. type GrantType string @@ -1648,6 +1741,34 @@ func (t *ExposureInfo) MergeEventExposureInfo(v EventExposureInfo) error { return err } +// AsFileExposureInfo returns the union data inside the ExposureInfo as a FileExposureInfo +func (t ExposureInfo) AsFileExposureInfo() (FileExposureInfo, error) { + var body FileExposureInfo + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromFileExposureInfo overwrites any union data inside the ExposureInfo as the provided FileExposureInfo +func (t *ExposureInfo) FromFileExposureInfo(v FileExposureInfo) error { + v.Type = "file" + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeFileExposureInfo performs a merge with any union data inside the ExposureInfo, using the provided FileExposureInfo +func (t *ExposureInfo) MergeFileExposureInfo(v FileExposureInfo) error { + v.Type = "file" + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + func (t ExposureInfo) Discriminator() (string, error) { var discriminator struct { Discriminator string `json:"type"` @@ -1666,6 +1787,8 @@ func (t ExposureInfo) ValueByDiscriminator() (interface{}, error) { return t.AsApiExposureInfo() case "event": return t.AsEventExposureInfo() + case "file": + return t.AsFileExposureInfo() default: return nil, errors.New("unknown discriminator value: " + discriminator) } @@ -1945,6 +2068,34 @@ func (t *SubscriptionInfo) MergeEventSubscriptionInfo(v EventSubscriptionInfo) e return err } +// AsFileSubscriptionInfo returns the union data inside the SubscriptionInfo as a FileSubscriptionInfo +func (t SubscriptionInfo) AsFileSubscriptionInfo() (FileSubscriptionInfo, error) { + var body FileSubscriptionInfo + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromFileSubscriptionInfo overwrites any union data inside the SubscriptionInfo as the provided FileSubscriptionInfo +func (t *SubscriptionInfo) FromFileSubscriptionInfo(v FileSubscriptionInfo) error { + v.Type = "file" + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeFileSubscriptionInfo performs a merge with any union data inside the SubscriptionInfo, using the provided FileSubscriptionInfo +func (t *SubscriptionInfo) MergeFileSubscriptionInfo(v FileSubscriptionInfo) error { + v.Type = "file" + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + func (t SubscriptionInfo) Discriminator() (string, error) { var discriminator struct { Discriminator string `json:"type"` @@ -1963,6 +2114,8 @@ func (t SubscriptionInfo) ValueByDiscriminator() (interface{}, error) { return t.AsApiSubscriptionInfo() case "event": return t.AsEventSubscriptionInfo() + case "file": + return t.AsFileSubscriptionInfo() default: return nil, errors.New("unknown discriminator value: " + discriminator) } @@ -1981,152 +2134,152 @@ func (t *SubscriptionInfo) UnmarshalJSON(b []byte) error { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "yNrth9v/DQAA//+C2Rh/Ng8BAA==", + "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", } // GetSwagger returns the content of the embedded swagger specification file From 5e4f0ad69f189cd06208e4c196e8b53e1fb83e02 Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Tue, 30 Jun 2026 10:43:01 +0300 Subject: [PATCH 03/31] feat(dhei-20905): add copyright header --- rover-server/internal/api/server.gen.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rover-server/internal/api/server.gen.go b/rover-server/internal/api/server.gen.go index 0a3624d22..2f0d6f26b 100644 --- a/rover-server/internal/api/server.gen.go +++ b/rover-server/internal/api/server.gen.go @@ -1,3 +1,7 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + // Package api provides primitives to interact with the openapi HTTP API. // // Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.6.1-0.20260425061340-097bc33cf46c DO NOT EDIT. From c33ed13d8a4b6e11410c4a52160145ab87d24a36 Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Tue, 30 Jun 2026 11:29:15 +0300 Subject: [PATCH 04/31] feat(dhei-20905): delete memeories dds from OS --- rover-server/api/openapi.yaml | 8 +- rover-server/internal/api/server.gen.go | 189 ++++++++++++------------ 2 files changed, 99 insertions(+), 98 deletions(-) diff --git a/rover-server/api/openapi.yaml b/rover-server/api/openapi.yaml index 2c21455ed..d433dd013 100644 --- a/rover-server/api/openapi.yaml +++ b/rover-server/api/openapi.yaml @@ -1753,8 +1753,8 @@ components: variant: type: string enum: - - dds - description: File-transfer backend. Optional; currently only "dds" is supported. + - sftp + description: File-transfer backend. Optional; currently only "sftp" is supported. visibility: $ref: '#/components/schemas/Visibility' publicKeys: @@ -2470,8 +2470,8 @@ components: variant: type: string enum: - - dds - description: File-transfer backend. Optional; currently only "dds" is supported. + - sftp + description: File-transfer backend. Optional; currently only "sftp" is supported. publicKeys: type: array items: diff --git a/rover-server/internal/api/server.gen.go b/rover-server/internal/api/server.gen.go index 2f0d6f26b..1fe3e439d 100644 --- a/rover-server/internal/api/server.gen.go +++ b/rover-server/internal/api/server.gen.go @@ -164,13 +164,13 @@ func (e EventTriggerResponseFilterMode) Valid() bool { // Defines values for FileExposureVariant. const ( - FileExposureVariantDds FileExposureVariant = "dds" + FileExposureVariantSftp FileExposureVariant = "sftp" ) // Valid indicates whether the value is a known member of the FileExposureVariant enum. func (e FileExposureVariant) Valid() bool { switch e { - case FileExposureVariantDds: + case FileExposureVariantSftp: return true default: return false @@ -194,13 +194,13 @@ func (e FileExposureInfoType) Valid() bool { // Defines values for FileExposureInfoVariant. const ( - FileExposureInfoVariantDds FileExposureInfoVariant = "dds" + FileExposureInfoVariantSftp FileExposureInfoVariant = "sftp" ) // Valid indicates whether the value is a known member of the FileExposureInfoVariant enum. func (e FileExposureInfoVariant) Valid() bool { switch e { - case FileExposureInfoVariantDds: + case FileExposureInfoVariantSftp: return true default: return false @@ -209,13 +209,13 @@ func (e FileExposureInfoVariant) Valid() bool { // Defines values for FileSubscriptionVariant. const ( - FileSubscriptionVariantDds FileSubscriptionVariant = "dds" + FileSubscriptionVariantSftp FileSubscriptionVariant = "sftp" ) // Valid indicates whether the value is a known member of the FileSubscriptionVariant enum. func (e FileSubscriptionVariant) Valid() bool { switch e { - case FileSubscriptionVariantDds: + case FileSubscriptionVariantSftp: return true default: return false @@ -239,13 +239,13 @@ func (e FileSubscriptionInfoType) Valid() bool { // Defines values for FileSubscriptionInfoVariant. const ( - FileSubscriptionInfoVariantDds FileSubscriptionInfoVariant = "dds" + FileSubscriptionInfoVariantSftp FileSubscriptionInfoVariant = "sftp" ) // Valid indicates whether the value is a known member of the FileSubscriptionInfoVariant enum. func (e FileSubscriptionInfoVariant) Valid() bool { switch e { - case FileSubscriptionInfoVariantDds: + case FileSubscriptionInfoVariantSftp: return true default: return false @@ -1045,12 +1045,12 @@ type FileExposure struct { PublicKeys []PublicKey `json:"publicKeys"` Type string `json:"type"` - // Variant File-transfer backend. Optional; currently only "dds" is supported. + // Variant File-transfer backend. Optional; currently only "sftp" is supported. Variant FileExposureVariant `json:"variant,omitempty,omitzero"` Visibility Visibility `json:"visibility,omitempty,omitzero"` } -// FileExposureVariant File-transfer backend. Optional; currently only "dds" is supported. +// FileExposureVariant File-transfer backend. Optional; currently only "sftp" is supported. type FileExposureVariant string // FileExposureInfo defines model for FileExposureInfo. @@ -1059,7 +1059,7 @@ type FileExposureInfo struct { PublicKeys []PublicKey `json:"publicKeys"` Type FileExposureInfoType `json:"type"` - // Variant File-transfer backend. Optional; currently only "dds" is supported. + // Variant File-transfer backend. Optional; currently only "sftp" is supported. Variant FileExposureInfoVariant `json:"variant,omitempty,omitzero"` Visibility Visibility `json:"visibility,omitempty,omitzero"` } @@ -1067,7 +1067,7 @@ type FileExposureInfo struct { // FileExposureInfoType defines model for FileExposureInfo.Type. type FileExposureInfoType string -// FileExposureInfoVariant File-transfer backend. Optional; currently only "dds" is supported. +// FileExposureInfoVariant File-transfer backend. Optional; currently only "sftp" is supported. type FileExposureInfoVariant string // FileSubscription defines model for FileSubscription. @@ -1076,11 +1076,11 @@ type FileSubscription struct { PublicKeys []PublicKey `json:"publicKeys"` Type string `json:"type"` - // Variant File-transfer backend. Optional; currently only "dds" is supported. + // Variant File-transfer backend. Optional; currently only "sftp" is supported. Variant FileSubscriptionVariant `json:"variant,omitempty,omitzero"` } -// FileSubscriptionVariant File-transfer backend. Optional; currently only "dds" is supported. +// FileSubscriptionVariant File-transfer backend. Optional; currently only "sftp" is supported. type FileSubscriptionVariant string // FileSubscriptionInfo defines model for FileSubscriptionInfo. @@ -1089,14 +1089,14 @@ type FileSubscriptionInfo struct { PublicKeys []PublicKey `json:"publicKeys"` Type FileSubscriptionInfoType `json:"type"` - // Variant File-transfer backend. Optional; currently only "dds" is supported. + // Variant File-transfer backend. Optional; currently only "sftp" is supported. Variant FileSubscriptionInfoVariant `json:"variant,omitempty,omitzero"` } // FileSubscriptionInfoType defines model for FileSubscriptionInfo.Type. type FileSubscriptionInfoType string -// FileSubscriptionInfoVariant File-transfer backend. Optional; currently only "dds" is supported. +// FileSubscriptionInfoVariant File-transfer backend. Optional; currently only "sftp" is supported. type FileSubscriptionInfoVariant string // GrantType defines model for GrantType. @@ -2206,84 +2206,85 @@ var swaggerSpec = []string{ "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", + "EQEA", } // GetSwagger returns the content of the embedded swagger specification file From 360fc12b0de22dbd028efb23feaef5945fcbe3c1 Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Wed, 1 Jul 2026 12:23:05 +0300 Subject: [PATCH 05/31] feat(dhei-20905): add mapping for FileExposure and FileSubscription and tests --- .../rover/in/__snapshots__/exposure_test.snap | 17 ++++++++++ .../rover/in/__snapshots__/rover_test.snap | 10 ++++++ .../in/__snapshots__/subscription_test.snap | 16 +++++++++ .../internal/mapper/rover/in/exposure.go | 33 +++++++++++++++++++ .../internal/mapper/rover/in/exposure_test.go | 10 ++++++ .../internal/mapper/rover/in/subscription.go | 16 +++++++++ .../mapper/rover/in/subscription_test.go | 10 ++++++ .../mapper/rover/in/suite_rover_in_test.go | 33 +++++++++++++++++++ 8 files changed, 145 insertions(+) diff --git a/rover-server/internal/mapper/rover/in/__snapshots__/exposure_test.snap b/rover-server/internal/mapper/rover/in/__snapshots__/exposure_test.snap index c34911537..425d2dc3c 100755 --- a/rover-server/internal/mapper/rover/in/__snapshots__/exposure_test.snap +++ b/rover-server/internal/mapper/rover/in/__snapshots__/exposure_test.snap @@ -46,6 +46,7 @@ Security: (*v1.Security)(nil), }, Event: (*v1.EventExposure)(nil), + File: (*v1.FileExposure)(nil), } --- @@ -62,9 +63,25 @@ Scopes: nil, AdditionalPublisherIds: nil, }, + File: (*v1.FileExposure)(nil), } --- [Exposure Mapper mapExposure must return an error for unknown exposure type - 1] &v1.Exposure{} --- + +[Exposure Mapper mapExposure must map a FileExposure correctly - 1] +&v1.Exposure{ + Api: (*v1.ApiExposure)(nil), + Event: (*v1.EventExposure)(nil), + File: &v1.FileExposure{ + FileType: "demo-sftp-spec-v1", + Variant: "sftp", + Visibility: "World", + PublicKeys: { + {Label:"provider-key", Key:"ssh-ed25519 AAAA-provider"}, + }, + }, +} +--- diff --git a/rover-server/internal/mapper/rover/in/__snapshots__/rover_test.snap b/rover-server/internal/mapper/rover/in/__snapshots__/rover_test.snap index 2cef8991f..6724a9da9 100644 --- a/rover-server/internal/mapper/rover/in/__snapshots__/rover_test.snap +++ b/rover-server/internal/mapper/rover/in/__snapshots__/rover_test.snap @@ -25,6 +25,7 @@ Security: (*v1.Security)(nil), }, Event: (*v1.EventExposure)(nil), + File: (*v1.FileExposure)(nil), }, }, Subscriptions: { @@ -37,6 +38,7 @@ Security: (*v1.SubscriberSecurity)(nil), }, Event: (*v1.EventSubscription)(nil), + File: (*v1.FileSubscription)(nil), }, }, Permissions: nil, @@ -72,6 +74,7 @@ Security: (*v1.Security)(nil), }, Event: (*v1.EventExposure)(nil), + File: (*v1.FileExposure)(nil), }, }, Subscriptions: nil, @@ -102,6 +105,7 @@ Security: (*v1.SubscriberSecurity)(nil), }, Event: (*v1.EventSubscription)(nil), + File: (*v1.FileSubscription)(nil), }, }, Permissions: nil, @@ -153,6 +157,7 @@ Security: (*v1.Security)(nil), }, Event: (*v1.EventExposure)(nil), + File: (*v1.FileExposure)(nil), }, }, Subscriptions: { @@ -165,6 +170,7 @@ Security: (*v1.SubscriberSecurity)(nil), }, Event: (*v1.EventSubscription)(nil), + File: (*v1.FileSubscription)(nil), }, }, Permissions: nil, @@ -241,6 +247,7 @@ Security: (*v1.Security)(nil), }, Event: (*v1.EventExposure)(nil), + File: (*v1.FileExposure)(nil), }, }, Subscriptions: { @@ -253,6 +260,7 @@ Security: (*v1.SubscriberSecurity)(nil), }, Event: (*v1.EventSubscription)(nil), + File: (*v1.FileSubscription)(nil), }, }, Permissions: nil, @@ -304,6 +312,7 @@ Security: (*v1.Security)(nil), }, Event: (*v1.EventExposure)(nil), + File: (*v1.FileExposure)(nil), }, }, Subscriptions: { @@ -316,6 +325,7 @@ Security: (*v1.SubscriberSecurity)(nil), }, Event: (*v1.EventSubscription)(nil), + File: (*v1.FileSubscription)(nil), }, }, Permissions: nil, diff --git a/rover-server/internal/mapper/rover/in/__snapshots__/subscription_test.snap b/rover-server/internal/mapper/rover/in/__snapshots__/subscription_test.snap index 67a3c9d67..ce888de59 100755 --- a/rover-server/internal/mapper/rover/in/__snapshots__/subscription_test.snap +++ b/rover-server/internal/mapper/rover/in/__snapshots__/subscription_test.snap @@ -23,6 +23,7 @@ Security: (*v1.SubscriberSecurity)(nil), }, Event: (*v1.EventSubscription)(nil), + File: (*v1.FileSubscription)(nil), } --- @@ -35,9 +36,24 @@ Trigger: (*v1.EventTrigger)(nil), Scopes: nil, }, + File: (*v1.FileSubscription)(nil), } --- [Subscription Mapper mapSubscription must return an error if Discriminator fails - 1] &v1.Subscription{} --- + +[Subscription Mapper mapSubscription must map a FileSubscription correctly - 1] +&v1.Subscription{ + Api: (*v1.ApiSubscription)(nil), + Event: (*v1.EventSubscription)(nil), + File: &v1.FileSubscription{ + FileType: "demo-sftp-spec-v1", + Variant: "sftp", + PublicKeys: { + {Label:"consumer-key", Key:"ssh-ed25519 AAAA-consumer"}, + }, + }, +} +--- diff --git a/rover-server/internal/mapper/rover/in/exposure.go b/rover-server/internal/mapper/rover/in/exposure.go index 7d21a044b..08f64a3f8 100644 --- a/rover-server/internal/mapper/rover/in/exposure.go +++ b/rover-server/internal/mapper/rover/in/exposure.go @@ -53,6 +53,14 @@ func mapExposure(in *api.Exposure, out *roverv1.Exposure) error { out.Event = mapEventExposure(eventExp) + case "file": + fileExp, err := in.AsFileExposure() + if err != nil { + return errors.Wrap(err, "failed to convert to FileExposure") + } + + out.File = mapFileExposure(fileExp) + default: return errors.Errorf("unknown exposure type: %s", expType) } @@ -60,6 +68,31 @@ func mapExposure(in *api.Exposure, out *roverv1.Exposure) error { return nil } +func mapFileExposure(in api.FileExposure) *roverv1.FileExposure { + out := &roverv1.FileExposure{ + FileType: in.FileType, + Variant: roverv1.FileVariant(in.Variant), + Visibility: toRoverVisibility(in.Visibility), + PublicKeys: mapPublicKeys(in.PublicKeys), + } + + return out +} + +func mapPublicKeys(in []api.PublicKey) []roverv1.PublicKey { + if len(in) == 0 { + return nil + } + out := make([]roverv1.PublicKey, len(in)) + for i, key := range in { + out[i] = roverv1.PublicKey{ + Label: key.Label, + Key: key.Key, + } + } + return out +} + func mapApiExposure(in api.ApiExposure) *roverv1.ApiExposure { out := &roverv1.ApiExposure{} out.BasePath = in.BasePath diff --git a/rover-server/internal/mapper/rover/in/exposure_test.go b/rover-server/internal/mapper/rover/in/exposure_test.go index 0d6855738..b9f2ae00e 100644 --- a/rover-server/internal/mapper/rover/in/exposure_test.go +++ b/rover-server/internal/mapper/rover/in/exposure_test.go @@ -96,6 +96,16 @@ var _ = Describe("Exposure Mapper", func() { snaps.MatchSnapshot(GinkgoT(), output) }) + It("must map a FileExposure correctly", func() { + input := GetFileExposure(fileExposure) + output := &roverv1.Exposure{} + + err := mapExposure(&input, output) + + Expect(err).To(BeNil()) + snaps.MatchSnapshot(GinkgoT(), output) + }) + It("must return an error for unknown exposure type", func() { input := &api.Exposure{} output := &roverv1.Exposure{} diff --git a/rover-server/internal/mapper/rover/in/subscription.go b/rover-server/internal/mapper/rover/in/subscription.go index 754364dd4..552f45d5d 100644 --- a/rover-server/internal/mapper/rover/in/subscription.go +++ b/rover-server/internal/mapper/rover/in/subscription.go @@ -35,6 +35,14 @@ func mapSubscription(in *api.Subscription, out *roverv1.Subscription) error { out.Event = mapEventSubscription(eventSub) + case "file": + fileSub, err := in.AsFileSubscription() + if err != nil { + return errors.Wrap(err, "failed to convert to FileSubscription") + } + + out.File = mapFileSubscription(fileSub) + default: return errors.Errorf("unknown subscription type: %s", subType) @@ -43,6 +51,14 @@ func mapSubscription(in *api.Subscription, out *roverv1.Subscription) error { return nil } +func mapFileSubscription(in api.FileSubscription) *roverv1.FileSubscription { + return &roverv1.FileSubscription{ + FileType: in.FileType, + Variant: roverv1.FileVariant(in.Variant), + PublicKeys: mapPublicKeys(in.PublicKeys), + } +} + func mapApiSubscription(in api.ApiSubscription) *roverv1.ApiSubscription { out := &roverv1.ApiSubscription{} out.BasePath = in.BasePath diff --git a/rover-server/internal/mapper/rover/in/subscription_test.go b/rover-server/internal/mapper/rover/in/subscription_test.go index 1b1fad4ab..a63a48f2a 100644 --- a/rover-server/internal/mapper/rover/in/subscription_test.go +++ b/rover-server/internal/mapper/rover/in/subscription_test.go @@ -55,6 +55,16 @@ var _ = Describe("Subscription Mapper", func() { snaps.MatchSnapshot(GinkgoT(), output) }) + It("must map a FileSubscription correctly", func() { + input := GetFileSubscription(fileSubscription) + output := &roverv1.Subscription{} + + err := mapSubscription(&input, output) + + Expect(err).To(BeNil()) + snaps.MatchSnapshot(GinkgoT(), output) + }) + It("must return an error if Discriminator fails", func() { input := &api.Subscription{} output := &roverv1.Subscription{} diff --git a/rover-server/internal/mapper/rover/in/suite_rover_in_test.go b/rover-server/internal/mapper/rover/in/suite_rover_in_test.go index b45d52726..f5680961c 100644 --- a/rover-server/internal/mapper/rover/in/suite_rover_in_test.go +++ b/rover-server/internal/mapper/rover/in/suite_rover_in_test.go @@ -40,6 +40,25 @@ var ( EventType: "test-event", } + fileExposure = api.FileExposure{ + Type: "file", + FileType: "demo-sftp-spec-v1", + Variant: "sftp", + Visibility: "World", + PublicKeys: []api.PublicKey{ + {Label: "provider-key", Key: "ssh-ed25519 AAAA-provider"}, + }, + } + + fileSubscription = api.FileSubscription{ + Type: "file", + FileType: "demo-sftp-spec-v1", + Variant: "sftp", + PublicKeys: []api.PublicKey{ + {Label: "consumer-key", Key: "ssh-ed25519 AAAA-consumer"}, + }, + } + resourceIdInfo = mapper.ResourceIdInfo{ Name: "rover-local-sub", Environment: "poc", @@ -93,3 +112,17 @@ func GetEventSubscription(eventSubscription api.EventSubscription) api.Subscript Expect(err).To(BeNil()) return sub } + +func GetFileExposure(fileExposure api.FileExposure) api.Exposure { + var exp api.Exposure + err := (&exp).FromFileExposure(fileExposure) + Expect(err).To(BeNil()) + return exp +} + +func GetFileSubscription(fileSubscription api.FileSubscription) api.Subscription { + var sub api.Subscription + err := (&sub).FromFileSubscription(fileSubscription) + Expect(err).To(BeNil()) + return sub +} From a5ba5da76b18821041f73c5ffc11a29cd3a8a550 Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Wed, 1 Jul 2026 18:50:51 +0300 Subject: [PATCH 06/31] feat(dhei-20905): add webhook and validation for file types add mapping for exp/subs into the rover --- rover-ctl/pkg/handlers/v0/rover.go | 4 + rover-ctl/pkg/handlers/v0/rover_test.go | 42 ++++++ rover/cmd/main.go | 7 + .../webhook/v1/filespecification_webhook.go | 69 ++++++++++ .../v1/filespecification_webhook_test.go | 121 ++++++++++++++++++ rover/internal/webhook/v1/rover_webhook.go | 86 +++++++++++++ 6 files changed, 329 insertions(+) create mode 100644 rover/internal/webhook/v1/filespecification_webhook.go create mode 100644 rover/internal/webhook/v1/filespecification_webhook_test.go diff --git a/rover-ctl/pkg/handlers/v0/rover.go b/rover-ctl/pkg/handlers/v0/rover.go index 80352c478..fd9af4bed 100644 --- a/rover-ctl/pkg/handlers/v0/rover.go +++ b/rover-ctl/pkg/handlers/v0/rover.go @@ -120,6 +120,8 @@ func PatchExposures(exposures []any) []map[string]any { exposuresMaps[i]["type"] = "api" } else if _, exist := exposure["eventType"]; exist { exposuresMaps[i]["type"] = "event" + } else if _, exist := exposure["fileType"]; exist { + exposuresMaps[i]["type"] = "file" } // TODO: add more types as needed security, exist := exposure["security"] if exist { @@ -147,6 +149,8 @@ func PatchSubscriptions(subscriptions []any) []map[string]any { subscriptionsMaps[i]["type"] = "api" } else if _, exist := subscription["eventType"]; exist { subscriptionsMaps[i]["type"] = "event" + } else if _, exist := subscription["fileType"]; exist { + subscriptionsMaps[i]["type"] = "file" } // TODO: add more types as needed security, exist := subscription["security"] if exist { diff --git a/rover-ctl/pkg/handlers/v0/rover_test.go b/rover-ctl/pkg/handlers/v0/rover_test.go index dc77d3aea..024ec5408 100644 --- a/rover-ctl/pkg/handlers/v0/rover_test.go +++ b/rover-ctl/pkg/handlers/v0/rover_test.go @@ -204,6 +204,34 @@ var _ = Describe("Rover Handler", func() { security := exposure["security"].(map[string]any) Expect(security).To(HaveKeyWithValue("type", "basicAuth")) }) + + It("should patch file exposures", func() { + obj := &types.UnstructuredObject{ + Content: map[string]any{ + "spec": map[string]any{ + "exposures": []any{ + map[string]any{ + "fileType": "demo-sftp-spec-v1", + "variant": "sftp", + }, + }, + }, + }, + } + + err := v0.PatchRoverRequest(context.Background(), obj) + + Expect(err).NotTo(HaveOccurred()) + + content := obj.GetContent() + Expect(content).To(HaveKey("exposures")) + + exposures := content["exposures"].([]map[string]any) + Expect(exposures).To(HaveLen(1)) + + exposure := exposures[0] + Expect(exposure).To(HaveKeyWithValue("type", "file")) + }) }) Context("when processing invalid rover spec", func() { @@ -307,6 +335,20 @@ var _ = Describe("Rover Handler", func() { Expect(result[0]["security"]).To(HaveKeyWithValue("type", "basicAuth")) }) + It("should patch file subscriptions correctly", func() { + subscriptions := []any{ + map[string]any{ + "fileType": "demo-sftp-spec-v1", + "variant": "sftp", + }, + } + + result := v0.PatchSubscriptions(subscriptions) + + Expect(result).To(HaveLen(1)) + Expect(result[0]).To(HaveKeyWithValue("type", "file")) + }) + It("should handle nil subscriptions", func() { // Test with nil result := v0.PatchSubscriptions(nil) diff --git a/rover/cmd/main.go b/rover/cmd/main.go index 6855d5080..7a0a50b17 100644 --- a/rover/cmd/main.go +++ b/rover/cmd/main.go @@ -237,6 +237,13 @@ func main() { os.Exit(1) } } + // nolint:goconst + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err := webhookv1.SetupFileSpecificationWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "FileSpecification") + os.Exit(1) + } + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/rover/internal/webhook/v1/filespecification_webhook.go b/rover/internal/webhook/v1/filespecification_webhook.go new file mode 100644 index 000000000..0a743f102 --- /dev/null +++ b/rover/internal/webhook/v1/filespecification_webhook.go @@ -0,0 +1,69 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/telekom/controlplane/common/pkg/controller" + cerrors "github.com/telekom/controlplane/common/pkg/errors" + roverv1 "github.com/telekom/controlplane/rover/api/v1" +) + +// SetupFileSpecificationWebhookWithManager registers the webhook for FileSpecification in the manager. +func SetupFileSpecificationWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr, &roverv1.FileSpecification{}). + WithValidator(&FileSpecificationCustomValidator{client: mgr.GetClient()}). + Complete() +} + +// +kubebuilder:webhook:path=/validate-rover-cp-ei-telekom-de-v1-filespecification,mutating=false,failurePolicy=fail,sideEffects=None,groups=rover.cp.ei.telekom.de,resources=filespecifications,verbs=create;update,versions=v1,name=vfilespecification-v1.kb.io,admissionReviewVersions=v1 + +type FileSpecificationCustomValidator struct { + client client.Client +} + +var _ admission.Validator[*roverv1.FileSpecification] = &FileSpecificationCustomValidator{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type FileSpecification. +func (v *FileSpecificationCustomValidator) ValidateCreate(ctx context.Context, filespecification *roverv1.FileSpecification) (admission.Warnings, error) { + return v.ValidateCreateOrUpdate(ctx, filespecification) +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type FileSpecification. +func (v *FileSpecificationCustomValidator) ValidateUpdate(ctx context.Context, _ *roverv1.FileSpecification, filespecification *roverv1.FileSpecification) (admission.Warnings, error) { + return v.ValidateCreateOrUpdate(ctx, filespecification) +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type FileSpecification. +func (v *FileSpecificationCustomValidator) ValidateDelete(ctx context.Context, filespecification *roverv1.FileSpecification) (admission.Warnings, error) { + return nil, nil +} + +func (v *FileSpecificationCustomValidator) ValidateCreateOrUpdate(ctx context.Context, filespecification *roverv1.FileSpecification) (admission.Warnings, error) { + + if controller.IsBeingDeleted(filespecification) { + return nil, nil + } + + valErr := cerrors.NewValidationError(roverv1.GroupVersion.WithKind("FileSpecification").GroupKind(), filespecification) + + // metadata.name must be equal to spec.type + if filespecification.GetName() != filespecification.Spec.Type { + valErr.AddInvalidError( + field.NewPath("spec").Child("type"), + filespecification.Spec.Type, + fmt.Sprintf("spec.type must be equal to metadata.name %q", filespecification.GetName()), + ) + } + + return valErr.BuildWarnings(), valErr.BuildError() +} diff --git a/rover/internal/webhook/v1/filespecification_webhook_test.go b/rover/internal/webhook/v1/filespecification_webhook_test.go new file mode 100644 index 000000000..cddb2064c --- /dev/null +++ b/rover/internal/webhook/v1/filespecification_webhook_test.go @@ -0,0 +1,121 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + + cerrors "github.com/telekom/controlplane/common/pkg/errors" + roverv1 "github.com/telekom/controlplane/rover/api/v1" +) + +var _ = Describe("File Type (SFTP) Validation", func() { + + newValErr := func() *cerrors.ValidationError { + return cerrors.NewValidationError(roverv1.GroupVersion.WithKind("Rover").GroupKind(), NewRover(testZone)) + } + + Context("validateFilePublicKeys", func() { + filePath := field.NewPath("spec").Child("exposures").Index(0).Child("file") + + It("should require at least one public key", func() { + valErr := newValErr() + validateFilePublicKeys(valErr, nil, filePath) + err := valErr.BuildError() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("at least one public key must be specified")) + }) + + It("should accept unique labels and key values", func() { + valErr := newValErr() + keys := []roverv1.PublicKey{ + {Label: "provider-key", Key: "ssh-ed25519 AAAA1"}, + {Label: "consumer-key", Key: "ssh-ed25519 AAAA2"}, + } + validateFilePublicKeys(valErr, keys, filePath) + Expect(valErr.BuildError()).NotTo(HaveOccurred()) + }) + + It("should reject duplicate public key labels per fileType", func() { + valErr := newValErr() + keys := []roverv1.PublicKey{ + {Label: "dup", Key: "ssh-ed25519 AAAA1"}, + {Label: "dup", Key: "ssh-ed25519 AAAA2"}, + } + validateFilePublicKeys(valErr, keys, filePath) + err := valErr.BuildError() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("labels must be unique per fileType")) + }) + + It("should reject duplicate public key values per fileType", func() { + valErr := newValErr() + keys := []roverv1.PublicKey{ + {Label: "key-a", Key: "ssh-ed25519 SAME"}, + {Label: "key-b", Key: "ssh-ed25519 SAME"}, + } + validateFilePublicKeys(valErr, keys, filePath) + err := valErr.BuildError() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("key values must be unique per fileType")) + }) + }) + + Context("MustNotHaveDuplicates for file types", func() { + It("should reject two subscriptions to the same fileType", func() { + valErr := newValErr() + subs := []roverv1.Subscription{ + {File: &roverv1.FileSubscription{FileType: "demo-sftp-spec-v1"}}, + {File: &roverv1.FileSubscription{FileType: "demo-sftp-spec-v1"}}, + } + Expect(MustNotHaveDuplicates(valErr, subs, nil)).To(Succeed()) + err := valErr.BuildError() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("duplicate subscription for file-type demo-sftp-spec-v1")) + }) + + It("should reject two exposures of the same fileType", func() { + valErr := newValErr() + exps := []roverv1.Exposure{ + {File: &roverv1.FileExposure{FileType: "demo-sftp-spec-v1"}}, + {File: &roverv1.FileExposure{FileType: "demo-sftp-spec-v1"}}, + } + Expect(MustNotHaveDuplicates(valErr, nil, exps)).To(Succeed()) + err := valErr.BuildError() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("duplicate exposure for file-type demo-sftp-spec-v1")) + }) + }) + + Context("FileSpecificationCustomValidator", func() { + var validator *FileSpecificationCustomValidator + + BeforeEach(func() { + validator = &FileSpecificationCustomValidator{client: k8sClient} + }) + + newFileSpec := func(name, specType string) *roverv1.FileSpecification { + return &roverv1.FileSpecification{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "default"}, + Spec: roverv1.FileSpecificationSpec{Type: specType, Version: "1.0.0"}, + } + } + + It("should accept a FileSpecification whose name equals spec.type", func() { + warnings, err := validator.ValidateCreate(ctx, newFileSpec("demo-sftp-spec-v1", "demo-sftp-spec-v1")) + Expect(warnings).To(BeNil()) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should reject a FileSpecification whose name differs from spec.type", func() { + _, err := validator.ValidateCreate(ctx, newFileSpec("demo-sftp-spec-v1", "other-type")) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("spec.type must be equal to metadata.name")) + }) + }) +}) diff --git a/rover/internal/webhook/v1/rover_webhook.go b/rover/internal/webhook/v1/rover_webhook.go index ff7cd329e..58082327f 100644 --- a/rover/internal/webhook/v1/rover_webhook.go +++ b/rover/internal/webhook/v1/rover_webhook.go @@ -200,6 +200,20 @@ func (r *RoverValidator) ValidateCreateOrUpdate(ctx context.Context, rover *rove } } + subscribesToFiles := slices.ContainsFunc(rover.Spec.Subscriptions, func(sub roverv1.Subscription) bool { + return sub.Type() == roverv1.TypeFile + }) + exposesFiles := slices.ContainsFunc(rover.Spec.Exposures, func(exp roverv1.Exposure) bool { + return exp.Type() == roverv1.TypeFile + }) + if (subscribesToFiles || exposesFiles) && !roverv1.IsFileTypeZoneSupported(rover.Spec.Zone) { + valErr.AddInvalidError( + field.NewPath("spec").Child("zone"), + rover.Spec.Zone, + fmt.Sprintf("zone '%s' does not support file types; only %s are supported", rover.Spec.Zone, strings.Join(roverv1.SupportedFileTypeZones, ", ")), + ) + } + if err := MustNotHaveDuplicates(valErr, rover.Spec.Subscriptions, rover.Spec.Exposures); err != nil { return nil, err } @@ -239,6 +253,8 @@ func (r *RoverValidator) ValidateExposure(ctx context.Context, valErr *cerrors.V return r.ValidateApiExposure(ctx, valErr, environment, exposure, zoneRef, idx) case roverv1.TypeEvent: return r.ValidateEventExposure(ctx, valErr, environment, exposure, zoneRef, idx) + case roverv1.TypeFile: + return r.ValidateFileExposure(valErr, exposure, idx) default: valErr.AddInvalidError( field.NewPath("spec").Child("exposures").Index(idx).Child("type"), @@ -393,6 +409,16 @@ func MustNotHaveDuplicates(valErr *cerrors.ValidationError, subs []roverv1.Subsc } existingSubs[sub.Event.EventType] = true } + + if sub.File != nil { + if _, exists := existingSubs[sub.File.FileType]; exists { + valErr.AddInvalidError( + field.NewPath("spec").Child("subscriptions").Index(idx).Child("file").Child("fileType"), + sub.File.FileType, fmt.Sprintf("duplicate subscription for file-type %s", sub.File.FileType), + ) + } + existingSubs[sub.File.FileType] = true + } } existingExps := make(map[string]bool) @@ -416,6 +442,16 @@ func MustNotHaveDuplicates(valErr *cerrors.ValidationError, subs []roverv1.Subsc } existingExps[exposure.Event.EventType] = true } + + if exposure.File != nil { + if _, exists := existingExps[exposure.File.FileType]; exists { + valErr.AddInvalidError( + field.NewPath("spec").Child("exposures").Index(idx).Child("file").Child("fileType"), + exposure.File.FileType, fmt.Sprintf("duplicate exposure for file-type %s", exposure.File.FileType), + ) + } + existingExps[exposure.File.FileType] = true + } } return nil @@ -577,7 +613,57 @@ func (r *RoverValidator) ValidateSubscription(ctx context.Context, valErr *cerro case roverv1.TypeEvent: // There is no special validation needed at this time. return nil + + case roverv1.TypeFile: + return r.ValidateFileSubscription(valErr, sub, idx) + } + + return nil +} + +func (r *RoverValidator) ValidateFileExposure(valErr *cerrors.ValidationError, exposure roverv1.Exposure, idx int) error { + if exposure.File == nil { + return nil } + validateFilePublicKeys(valErr, exposure.File.PublicKeys, field.NewPath("spec").Child("exposures").Index(idx).Child("file")) + return nil +} +func (r *RoverValidator) ValidateFileSubscription(valErr *cerrors.ValidationError, sub roverv1.Subscription, idx int) error { + if sub.File == nil { + return nil + } + + validateFilePublicKeys(valErr, sub.File.PublicKeys, field.NewPath("spec").Child("subscriptions").Index(idx).Child("file")) return nil } + +func validateFilePublicKeys(valErr *cerrors.ValidationError, keys []roverv1.PublicKey, filePath *field.Path) { + if len(keys) == 0 { + valErr.AddRequiredError(filePath.Child("publicKeys"), "at least one public key must be specified") + return + } + + seenLabels := make(map[string]struct{}, len(keys)) + seenKeys := make(map[string]struct{}, len(keys)) + for i, key := range keys { + keyPath := filePath.Child("publicKeys").Index(i) + if _, exists := seenLabels[key.Label]; exists { + valErr.AddInvalidError( + keyPath.Child("label"), + key.Label, + fmt.Sprintf("duplicate public key label '%s'; labels must be unique per fileType", key.Label), + ) + } + seenLabels[key.Label] = struct{}{} + + if _, exists := seenKeys[key.Key]; exists { + valErr.AddInvalidError( + keyPath.Child("key"), + key.Label, + fmt.Sprintf("duplicate public key value for label '%s'; key values must be unique per fileType", key.Label), + ) + } + seenKeys[key.Key] = struct{}{} + } +} From 9361abd49e310767f33675ee549703956aee26ae Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Fri, 3 Jul 2026 15:17:43 +0300 Subject: [PATCH 07/31] feat(dhei-20905): change config/feature comment --- common/pkg/config/feature.go | 2 +- file/api/go.mod | 74 +++++++ file/api/go.sum | 195 ++++++++++++++++++ file/api/v1/file_shared_types.go | 46 +++++ file/api/v1/fileexposure_types.go | 105 ++++++++++ file/api/v1/filesubscription_types.go | 85 ++++++++ file/api/v1/filetype_types.go | 94 +++++++++ file/api/v1/groupversion_info.go | 24 +++ .../file.ei.telekom.de_fileexposures.yaml | 189 +++++++++++++++++ .../file.ei.telekom.de_filesubscriptions.yaml | 138 +++++++++++++ .../bases/file.ei.telekom.de_filetypes.yaml | 136 ++++++++++++ file/config/crd/kustomization.yaml | 13 ++ 12 files changed, 1100 insertions(+), 1 deletion(-) create mode 100644 file/api/go.mod create mode 100644 file/api/go.sum create mode 100644 file/api/v1/file_shared_types.go create mode 100644 file/api/v1/fileexposure_types.go create mode 100644 file/api/v1/filesubscription_types.go create mode 100644 file/api/v1/filetype_types.go create mode 100644 file/api/v1/groupversion_info.go create mode 100644 file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml create mode 100644 file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml create mode 100644 file/config/crd/bases/file.ei.telekom.de_filetypes.yaml create mode 100644 file/config/crd/kustomization.yaml diff --git a/common/pkg/config/feature.go b/common/pkg/config/feature.go index 0418e5e85..57b553182 100644 --- a/common/pkg/config/feature.go +++ b/common/pkg/config/feature.go @@ -48,7 +48,7 @@ var ( FeaturePermission Feature = NewFeature("permission", false) // Permission feature disabled by default FeatureSecretManager Feature = NewFeature("secret_manager", true) // Secret Manager feature enabled by default FeatureFileManager Feature = NewFeature("file_manager", true) // File Manager feature enabled by default - // TODO(DHEI-20905): File/DDS domain feature disabled by default. The rover-domain + // TODO(DHEI-20905): File/SFTP domain feature disabled by default. The rover-domain // dispatch is in place, but the File domain (DHEI-20903) and SFTP/DDS domain // (DHEI-20904) are not yet available. Enable this flag once those domains land so // the rover operator can create file-domain resources instead of returning Blocked. diff --git a/file/api/go.mod b/file/api/go.mod new file mode 100644 index 000000000..26d52e322 --- /dev/null +++ b/file/api/go.mod @@ -0,0 +1,74 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +module github.com/telekom/controlplane/file/api + +go 1.26.4 + +require ( + github.com/onsi/ginkgo/v2 v2.28.3 + github.com/onsi/gomega v1.40.0 + github.com/telekom/controlplane/common v0.0.0 + k8s.io/apiextensions-apiserver v0.36.0 + k8s.io/apimachinery v0.36.0 + sigs.k8s.io/controller-runtime v0.24.0 +) + +require ( + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.36.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/term v0.43.0 // indirect + golang.org/x/text v0.37.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.44.0 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.36.0 // indirect + k8s.io/client-go v0.36.0 // indirect + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) + +replace github.com/telekom/controlplane/common => ../../common diff --git a/file/api/go.sum b/file/api/go.sum new file mode 100644 index 000000000..dca3badb6 --- /dev/null +++ b/file/api/go.sum @@ -0,0 +1,195 @@ +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.28.3 h1:4JvMdwtFU0imd8fHx25OJXoDMRexnf8v5NHKYSTTji4= +github.com/onsi/ginkgo/v2 v2.28.3/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc= +github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80= +k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34= +k8s.io/apiextensions-apiserver v0.36.0 h1:Wt7E8J+VBCbj4FjiBfDTK/neXDDjyJVJc7xfuOHImZ0= +k8s.io/apiextensions-apiserver v0.36.0/go.mod h1:kGDjH0msuiIB3tgsYRV0kS9GqpMYMUsQ3GHv7TApyug= +k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ= +k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc= +k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c= +k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +sigs.k8s.io/controller-runtime v0.24.0 h1:Ck6N2LdS8Lovy1o25BB4r1xjvLEKUl1s2o9kU+KWDE4= +sigs.k8s.io/controller-runtime v0.24.0/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/file/api/v1/file_shared_types.go b/file/api/v1/file_shared_types.go new file mode 100644 index 000000000..dd26ba352 --- /dev/null +++ b/file/api/v1/file_shared_types.go @@ -0,0 +1,46 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +// PublicKey is a labeled SSH public key registered on the SFTP user. +type PublicKey struct { + // Label is a human-readable identifier for the key. It must be unique per file type. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + Label string `json:"label"` + + // Key is the SSH public key value. It must be unique per file type. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + +// Visibility defines who can see and subscribe to an exposed file type. +// +kubebuilder:validation:Enum=World;Zone;Enterprise +type Visibility string + +const ( + VisibilityWorld Visibility = "World" + VisibilityZone Visibility = "Zone" + VisibilityEnterprise Visibility = "Enterprise" +) + +func (v Visibility) String() string { + return string(v) +} + +// ApprovalStrategy defines the approval mode for subscriptions to a file type exposure. +// +kubebuilder:validation:Enum=Auto;Simple;FourEyes +type ApprovalStrategy string + +const ( + ApprovalStrategyAuto ApprovalStrategy = "Auto" + ApprovalStrategySimple ApprovalStrategy = "Simple" + ApprovalStrategyFourEyes ApprovalStrategy = "FourEyes" +) + +func (a ApprovalStrategy) String() string { + return string(a) +} diff --git a/file/api/v1/fileexposure_types.go b/file/api/v1/fileexposure_types.go new file mode 100644 index 000000000..1d4f22a72 --- /dev/null +++ b/file/api/v1/fileexposure_types.go @@ -0,0 +1,105 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +import ( + ctypes "github.com/telekom/controlplane/common/pkg/types" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// FileExposureSpec defines the desired state of FileExposure. +// It is created in the file domain from a rover-domain Rover exposure (1:1). The +// derived logical Application is created without an Identity client. +type FileExposureSpec struct { + // Approval configures how subscriptions to this file type are approved. + // +kubebuilder:default=Simple + Approval ApprovalStrategy `json:"approval,omitempty"` + + // Visibility defines who can see and subscribe to this file type. + // +kubebuilder:default=Enterprise + Visibility Visibility `json:"visibility,omitempty"` + + // FileTypeRef references the file-domain FileType this exposure belongs to. + // It must live in the same namespace as this FileExposure. + // +kubebuilder:validation:Required + FileTypeRef ctypes.ObjectRef `json:"fileTypeRef"` + + // PublicKeys are the SSH public keys registered for the provider's SFTP user. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + PublicKeys []PublicKey `json:"publicKeys"` + + // ZoneServiceConfigRef references the zone-scoped service configuration + // (provided by the SFTP/DDS domain) used to reach the backend for this exposure. + // +optional + ZoneServiceConfigRef *ctypes.ObjectRef `json:"zoneServiceConfigRef,omitempty"` +} + +// FileExposureStatus defines the observed state of FileExposure. +type FileExposureStatus struct { + // +listType=map + // +listMapKey=type + // +patchStrategy=merge + // +patchMergeKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` + + // Active indicates whether this exposure has been provisioned. + Active bool `json:"active,omitempty"` + + // Subscriptions references the file-domain FileSubscriptions bound to this exposure. + // +optional + Subscriptions []ctypes.ObjectRef `json:"subscriptions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Active",type="boolean",JSONPath=".status.active",description="Whether this exposure is provisioned" +// +kubebuilder:printcolumn:name="CreatedAt",type="date",JSONPath=".metadata.creationTimestamp",description="Creation timestamp" + +// FileExposure is the Schema for the fileexposures API. +// It declares that an application exposes a file type. The derived logical +// Application is created without an Identity client. +type FileExposure struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FileExposureSpec `json:"spec,omitempty"` + Status FileExposureStatus `json:"status,omitempty"` +} + +var _ ctypes.Object = &FileExposure{} + +func (r *FileExposure) GetConditions() []metav1.Condition { + return r.Status.Conditions +} + +func (r *FileExposure) SetCondition(condition metav1.Condition) bool { + return meta.SetStatusCondition(&r.Status.Conditions, condition) +} + +// +kubebuilder:object:root=true + +// FileExposureList contains a list of FileExposure. +type FileExposureList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []FileExposure `json:"items"` +} + +var _ ctypes.ObjectList = &FileExposureList{} + +func (r *FileExposureList) GetItems() []ctypes.Object { + items := make([]ctypes.Object, len(r.Items)) + for i := range r.Items { + items[i] = &r.Items[i] + } + return items +} + +func init() { + SchemeBuilder.Register(&FileExposure{}, &FileExposureList{}) +} diff --git a/file/api/v1/filesubscription_types.go b/file/api/v1/filesubscription_types.go new file mode 100644 index 000000000..7ed2b595b --- /dev/null +++ b/file/api/v1/filesubscription_types.go @@ -0,0 +1,85 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +import ( + ctypes "github.com/telekom/controlplane/common/pkg/types" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// FileSubscriptionSpec defines the desired state of FileSubscription. +// It is created in the file domain from a rover-domain Rover subscription (1:1). +type FileSubscriptionSpec struct { + // FileTypeRef references the file-domain FileType this subscription belongs to. + // +kubebuilder:validation:Required + FileTypeRef ctypes.ObjectRef `json:"fileTypeRef"` + + // PublicKeys are the SSH public keys registered for the consumer's SFTP user. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + PublicKeys []PublicKey `json:"publicKeys"` +} + +// FileSubscriptionStatus defines the observed state of FileSubscription. +type FileSubscriptionStatus struct { + // +listType=map + // +listMapKey=type + // +patchStrategy=merge + // +patchMergeKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` + + // Active indicates whether this subscription has been provisioned. + Active bool `json:"active,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Active",type="boolean",JSONPath=".status.active",description="Whether this subscription is provisioned" +// +kubebuilder:printcolumn:name="CreatedAt",type="date",JSONPath=".metadata.creationTimestamp",description="Creation timestamp" + +// FileSubscription is the Schema for the filesubscriptions API. +// It declares that an application consumes a file type. +type FileSubscription struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FileSubscriptionSpec `json:"spec,omitempty"` + Status FileSubscriptionStatus `json:"status,omitempty"` +} + +var _ ctypes.Object = &FileSubscription{} + +func (r *FileSubscription) GetConditions() []metav1.Condition { + return r.Status.Conditions +} + +func (r *FileSubscription) SetCondition(condition metav1.Condition) bool { + return meta.SetStatusCondition(&r.Status.Conditions, condition) +} + +// +kubebuilder:object:root=true + +// FileSubscriptionList contains a list of FileSubscription. +type FileSubscriptionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []FileSubscription `json:"items"` +} + +var _ ctypes.ObjectList = &FileSubscriptionList{} + +func (r *FileSubscriptionList) GetItems() []ctypes.Object { + items := make([]ctypes.Object, len(r.Items)) + for i := range r.Items { + items[i] = &r.Items[i] + } + return items +} + +func init() { + SchemeBuilder.Register(&FileSubscription{}, &FileSubscriptionList{}) +} diff --git a/file/api/v1/filetype_types.go b/file/api/v1/filetype_types.go new file mode 100644 index 000000000..976f399b4 --- /dev/null +++ b/file/api/v1/filetype_types.go @@ -0,0 +1,94 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +import ( + ctypes "github.com/telekom/controlplane/common/pkg/types" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// FileTypeSpec defines the desired state of FileType. +// A FileType is the file-domain registry entry for a file type. It is created in the +// file domain from a rover-domain FileSpecification (1:1) and is the canonical +// resource that FileExposure (1:1) and FileSubscription (1:n) reference via their +// fileTypeRef (mirrors event.EventType). +type FileTypeSpec struct { + // Description provides a human-readable summary of this file type. + // +optional + Description string `json:"description,omitempty"` + + // ExposureRef references the file-domain FileExposure created for this file type (1:1). + // +optional + ExposureRef *ctypes.ObjectRef `json:"exposureRef,omitempty"` + + // SubscriptionRefs references the file-domain FileSubscriptions created for this + // file type (1:n). + // +optional + SubscriptionRefs []ctypes.ObjectRef `json:"subscriptionRefs,omitempty"` +} + +// FileTypeStatus defines the observed state of FileType. +type FileTypeStatus struct { + // +listType=map + // +listMapKey=type + // +patchStrategy=merge + // +patchMergeKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` + + // Active indicates whether this FileType has been provisioned. + Active bool `json:"active,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:shortName=ftype +// +kubebuilder:printcolumn:name="Active",type="boolean",JSONPath=".status.active",description="Whether this file type is provisioned" +// +kubebuilder:printcolumn:name="CreatedAt",type="date",JSONPath=".metadata.creationTimestamp",description="Creation timestamp" + +// FileType is the Schema for the filetypes API. +// It represents a registered file type in the file domain, serving as the canonical +// reference that FileExposure and FileSubscription point to (mirrors event.EventType). +type FileType struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FileTypeSpec `json:"spec,omitempty"` + Status FileTypeStatus `json:"status,omitempty"` +} + +var _ ctypes.Object = &FileType{} + +func (r *FileType) GetConditions() []metav1.Condition { + return r.Status.Conditions +} + +func (r *FileType) SetCondition(condition metav1.Condition) bool { + return meta.SetStatusCondition(&r.Status.Conditions, condition) +} + +// +kubebuilder:object:root=true + +// FileTypeList contains a list of FileType. +type FileTypeList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []FileType `json:"items"` +} + +var _ ctypes.ObjectList = &FileTypeList{} + +func (r *FileTypeList) GetItems() []ctypes.Object { + items := make([]ctypes.Object, len(r.Items)) + for i := range r.Items { + items[i] = &r.Items[i] + } + return items +} + +func init() { + SchemeBuilder.Register(&FileType{}, &FileTypeList{}) +} diff --git a/file/api/v1/groupversion_info.go b/file/api/v1/groupversion_info.go new file mode 100644 index 000000000..c8295bf23 --- /dev/null +++ b/file/api/v1/groupversion_info.go @@ -0,0 +1,24 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +// Package v1 contains API Schema definitions for the file v1 API group. +// +kubebuilder:object:generate=true +// +groupName=file.ei.telekom.de +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "file.ei.telekom.de", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml b/file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml new file mode 100644 index 000000000..fe4579da8 --- /dev/null +++ b/file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml @@ -0,0 +1,189 @@ +# SPDX-FileCopyrightText: 2026 Deutsche Telekom IT GmbH +# +# SPDX-License-Identifier: Apache-2.0 +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: fileexposures.file.ei.telekom.de +spec: + group: file.ei.telekom.de + names: + kind: FileExposure + listKind: FileExposureList + plural: fileexposures + singular: fileexposure + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Whether this exposure is provisioned + jsonPath: .status.active + name: Active + type: boolean + - description: Creation timestamp + jsonPath: .metadata.creationTimestamp + name: CreatedAt + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + FileExposure is the Schema for the fileexposures API. + It declares that an application exposes a file type. The derived logical + Application is created without an Identity client. + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + description: ExposureSpec defines the desired state of a file Exposure. + properties: + approval: + default: Simple + description: Approval configures how subscriptions to this file type + are approved. + enum: + - Auto + - Simple + - FourEyes + type: string + publicKeys: + description: PublicKeys are the SSH public keys registered for the + provider's SFTP user. + items: + description: PublicKey is a labeled SSH public key registered on + the SFTP user. + properties: + key: + description: Key is the SSH public key value. It must be unique + per file type. + minLength: 1 + type: string + label: + description: Label is a human-readable identifier for the key. + It must be unique per file type. + minLength: 1 + type: string + required: + - key + - label + type: object + minItems: 1 + type: array + fileTypeRef: + description: |- + FileTypeRef references the file-domain FileType this exposure belongs to. + It must live in the same namespace as this FileExposure. + properties: + name: + type: string + namespace: + type: string + uid: + type: string + required: + - name + - namespace + type: object + visibility: + default: Enterprise + description: Visibility defines who can see and subscribe to this file + type. + enum: + - World + - Zone + - Enterprise + type: string + zoneServiceConfigRef: + description: |- + ZoneServiceConfigRef references the zone-scoped service configuration + (provided by the SFTP/DDS domain) used to reach the backend for this exposure. + properties: + name: + type: string + namespace: + type: string + uid: + type: string + required: + - name + - namespace + type: object + required: + - publicKeys + - specRef + type: object + status: + description: ExposureStatus defines the observed state of a file Exposure. + properties: + active: + description: Active indicates whether this exposure has been provisioned. + type: boolean + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + subscriptions: + description: Subscriptions references the file-domain FileSubscriptions + bound to this exposure. + items: + properties: + name: + type: string + namespace: + type: string + uid: + type: string + required: + - name + - namespace + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml b/file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml new file mode 100644 index 000000000..dffe37be6 --- /dev/null +++ b/file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml @@ -0,0 +1,138 @@ +# SPDX-FileCopyrightText: 2026 Deutsche Telekom IT GmbH +# +# SPDX-License-Identifier: Apache-2.0 +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: filesubscriptions.file.ei.telekom.de +spec: + group: file.ei.telekom.de + names: + kind: FileSubscription + listKind: FileSubscriptionList + plural: filesubscriptions + singular: filesubscription + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Whether this subscription is provisioned + jsonPath: .status.active + name: Active + type: boolean + - description: Creation timestamp + jsonPath: .metadata.creationTimestamp + name: CreatedAt + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + FileSubscription is the Schema for the filesubscriptions API. + It declares that an application consumes a file type. + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + description: FileSubscriptionSpec defines the desired state of FileSubscription. + properties: + fileTypeRef: + description: FileTypeRef references the file-domain FileType this subscription + belongs to. + properties: + name: + type: string + namespace: + type: string + uid: + type: string + required: + - name + - namespace + type: object + publicKeys: + description: PublicKeys are the SSH public keys registered for the + consumer's SFTP user. + items: + description: PublicKey is a labeled SSH public key registered on + the SFTP user. + properties: + key: + description: Key is the SSH public key value. It must be unique + per file type. + minLength: 1 + type: string + label: + description: Label is a human-readable identifier for the key. + It must be unique per file type. + minLength: 1 + type: string + required: + - key + - label + type: object + minItems: 1 + type: array + required: + - fileTypeRef + - publicKeys + type: object + status: + description: FileSubscriptionStatus defines the observed state of FileSubscription. + properties: + active: + description: Active indicates whether this subscription has been provisioned. + type: boolean + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/file/config/crd/bases/file.ei.telekom.de_filetypes.yaml b/file/config/crd/bases/file.ei.telekom.de_filetypes.yaml new file mode 100644 index 000000000..47fbd05e0 --- /dev/null +++ b/file/config/crd/bases/file.ei.telekom.de_filetypes.yaml @@ -0,0 +1,136 @@ +# SPDX-FileCopyrightText: 2026 Deutsche Telekom IT GmbH +# +# SPDX-License-Identifier: Apache-2.0 +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: filetypes.file.ei.telekom.de +spec: + group: file.ei.telekom.de + names: + kind: FileType + listKind: FileTypeList + plural: filetypes + singular: filetype + shortNames: + - ftype + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Whether this file spec is provisioned + jsonPath: .status.active + name: Active + type: boolean + - description: Creation timestamp + jsonPath: .metadata.creationTimestamp + name: CreatedAt + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + FileType is the Schema for the filetypes API. + It represents a registered file type in the file domain, serving as the canonical + reference that FileExposure and FileSubscription point to (mirrors event.EventType). + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + description: FileTypeSpec defines the desired state of FileType. + properties: + description: + description: Description provides a human-readable summary of this + file type. + type: string + exposureRef: + description: ExposureRef references the file-domain FileExposure created + for this file type (1:1). + properties: + name: + type: string + namespace: + type: string + uid: + type: string + required: + - name + - namespace + type: object + subscriptionRefs: + description: SubscriptionRefs references the file-domain FileSubscriptions + created for this file type (1:n). + items: + properties: + name: + type: string + namespace: + type: string + uid: + type: string + required: + - name + - namespace + type: object + type: array + type: object + status: + description: FileTypeStatus defines the observed state of FileType. + properties: + active: + description: Active indicates whether this FileType has been provisioned. + type: boolean + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + type: object + served: true + storage: true + subresources: + status: {} + diff --git a/file/config/crd/kustomization.yaml b/file/config/crd/kustomization.yaml new file mode 100644 index 000000000..2a901b41f --- /dev/null +++ b/file/config/crd/kustomization.yaml @@ -0,0 +1,13 @@ +# Copyright 2026 Deutsche Telekom IT GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/file.ei.telekom.de_filetypes.yaml +- bases/file.ei.telekom.de_fileexposures.yaml +- bases/file.ei.telekom.de_filesubscriptions.yaml +# +kubebuilder:scaffold:crdkustomizeresource + From 4fa6383ad9271b134f771102253be569d4d3e7f8 Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Fri, 3 Jul 2026 18:20:02 +0300 Subject: [PATCH 08/31] feat(dhei-20905): file domain exp/subs handler change logic on client creation --- rover/internal/handler/rover/file/exposure.go | 72 +++++++++++++++++++ .../handler/rover/file/subscription.go | 68 ++++++++++++++++++ rover/internal/handler/rover/file/util.go | 28 ++++++++ .../internal/handler/rover/file/util_test.go | 56 +++++++++++++++ rover/internal/handler/rover/handler.go | 27 +++++++ 5 files changed, 251 insertions(+) create mode 100644 rover/internal/handler/rover/file/exposure.go create mode 100644 rover/internal/handler/rover/file/subscription.go create mode 100644 rover/internal/handler/rover/file/util.go create mode 100644 rover/internal/handler/rover/file/util_test.go diff --git a/rover/internal/handler/rover/file/exposure.go b/rover/internal/handler/rover/file/exposure.go new file mode 100644 index 000000000..fbe94399d --- /dev/null +++ b/rover/internal/handler/rover/file/exposure.go @@ -0,0 +1,72 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package file + +import ( + "context" + + "github.com/pkg/errors" + "github.com/telekom/controlplane/common/pkg/client" + "github.com/telekom/controlplane/common/pkg/config" + "github.com/telekom/controlplane/common/pkg/types" + "github.com/telekom/controlplane/common/pkg/util/labelutil" + filev1 "github.com/telekom/controlplane/file/api/v1" + roverv1 "github.com/telekom/controlplane/rover/api/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// HandleExposure creates or updates a file-domain FileExposure owned by the Rover. +func HandleExposure(ctx context.Context, c client.JanitorClient, owner *roverv1.Rover, exp *roverv1.FileExposure) error { + log := log.FromContext(ctx) + log.V(1).Info("Handle FileExposure", "fileType", exp.FileType) + + name := MakeName(exp.FileType, owner.Name) + + fileExposure := &filev1.FileExposure{ + ObjectMeta: metav1.ObjectMeta{ + Name: labelutil.NormalizeNameValue(name), + Namespace: owner.Namespace, + }, + } + + // The FileType is created from the FileSpecification in the same namespace + // as the Rover exposure, with its name derived from the file type identifier. + fileTypeRef := types.ObjectRef{ + Name: filev1.MakeFileTypeName(exp.FileType), + Namespace: owner.Namespace, + } + + mutator := func() error { + if err := controllerutil.SetControllerReference(owner, fileExposure, c.Scheme()); err != nil { + return errors.Wrap(err, "failed to set controller reference") + } + + fileExposure.Labels = map[string]string{ + filev1.FileTypeLabelKey: labelutil.NormalizeLabelValue(exp.FileType), + config.BuildLabelKey("zone"): labelutil.NormalizeLabelValue(owner.Spec.Zone), + config.BuildLabelKey("application"): labelutil.NormalizeLabelValue(owner.Name), + } + + fileExposure.Spec = filev1.FileExposureSpec{ + Approval: filev1.ApprovalStrategySimple, + Visibility: filev1.Visibility(exp.Visibility.String()), + FileTypeRef: fileTypeRef, + PublicKeys: mapPublicKeys(exp.PublicKeys), + } + return nil + } + + if _, err := c.CreateOrUpdate(ctx, fileExposure, mutator); err != nil { + return errors.Wrap(err, "failed to create or update FileExposure") + } + + owner.Status.FileExposures = append(owner.Status.FileExposures, types.ObjectRef{ + Name: fileExposure.Name, + Namespace: fileExposure.Namespace, + }) + return nil +} diff --git a/rover/internal/handler/rover/file/subscription.go b/rover/internal/handler/rover/file/subscription.go new file mode 100644 index 000000000..825b137d5 --- /dev/null +++ b/rover/internal/handler/rover/file/subscription.go @@ -0,0 +1,68 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package file + +import ( + "context" + + "github.com/pkg/errors" + "github.com/telekom/controlplane/common/pkg/client" + "github.com/telekom/controlplane/common/pkg/config" + "github.com/telekom/controlplane/common/pkg/types" + "github.com/telekom/controlplane/common/pkg/util/labelutil" + filev1 "github.com/telekom/controlplane/file/api/v1" + roverv1 "github.com/telekom/controlplane/rover/api/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// HandleSubscription creates or updates a file-domain FileSubscription owned by the Rover. +func HandleSubscription(ctx context.Context, c client.JanitorClient, owner *roverv1.Rover, sub *roverv1.FileSubscription) error { + log := log.FromContext(ctx) + log.V(1).Info("Handle FileSubscription", "fileType", sub.FileType) + + name := MakeName(sub.FileType, owner.Name) + + fileSubscription := &filev1.FileSubscription{ + ObjectMeta: metav1.ObjectMeta{ + Name: labelutil.NormalizeNameValue(name), + Namespace: owner.Namespace, + }, + } + + fileTypeRef := types.ObjectRef{ + Name: filev1.MakeFileTypeName(sub.FileType), + Namespace: owner.Namespace, + } + + mutator := func() error { + if err := controllerutil.SetControllerReference(owner, fileSubscription, c.Scheme()); err != nil { + return errors.Wrap(err, "failed to set controller reference") + } + + fileSubscription.Labels = map[string]string{ + filev1.FileTypeLabelKey: labelutil.NormalizeLabelValue(sub.FileType), + config.BuildLabelKey("zone"): labelutil.NormalizeLabelValue(owner.Spec.Zone), + config.BuildLabelKey("application"): labelutil.NormalizeLabelValue(owner.Name), + } + + fileSubscription.Spec = filev1.FileSubscriptionSpec{ + FileTypeRef: fileTypeRef, + PublicKeys: mapPublicKeys(sub.PublicKeys), + } + return nil + } + + if _, err := c.CreateOrUpdate(ctx, fileSubscription, mutator); err != nil { + return errors.Wrap(err, "failed to create or update FileSubscription") + } + + owner.Status.FileSubscriptions = append(owner.Status.FileSubscriptions, types.ObjectRef{ + Name: fileSubscription.Name, + Namespace: fileSubscription.Namespace, + }) + return nil +} diff --git a/rover/internal/handler/rover/file/util.go b/rover/internal/handler/rover/file/util.go new file mode 100644 index 000000000..25ca2a9f1 --- /dev/null +++ b/rover/internal/handler/rover/file/util.go @@ -0,0 +1,28 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package file + +import ( + filev1 "github.com/telekom/controlplane/file/api/v1" + roverv1 "github.com/telekom/controlplane/rover/api/v1" +) + +// MakeName generates a deterministic resource name for a file exposure or +// subscription: "--" (spec_dcp naming), normalized. +func MakeName(fileType, ownerName string) string { + return filev1.MakeFileTypeName(fileType) + "--" + ownerName +} + +// mapPublicKeys converts rover-domain public keys to file-domain public keys. +func mapPublicKeys(in []roverv1.PublicKey) []filev1.PublicKey { + if len(in) == 0 { + return nil + } + out := make([]filev1.PublicKey, len(in)) + for i, k := range in { + out[i] = filev1.PublicKey{Label: k.Label, Key: k.Key} + } + return out +} diff --git a/rover/internal/handler/rover/file/util_test.go b/rover/internal/handler/rover/file/util_test.go new file mode 100644 index 000000000..7ec5b488d --- /dev/null +++ b/rover/internal/handler/rover/file/util_test.go @@ -0,0 +1,56 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package file + +import ( + "testing" + + roverv1 "github.com/telekom/controlplane/rover/api/v1" +) + +func TestMakeName(t *testing.T) { + tests := []struct { + name string + fileType string + owner string + want string + }{ + {"hyphenated file type", "de-telekom-eni-foo-v1", "provider", "de-telekom-eni-foo-v1--provider"}, + {"dotted file type is normalized", "de.telekom.foo.v1", "consumer", "de-telekom-foo-v1--consumer"}, + {"mixed case is lowercased", "De.Telekom.V1", "app", "de-telekom-v1--app"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := MakeName(tt.fileType, tt.owner); got != tt.want { + t.Errorf("MakeName(%q, %q) = %q, want %q", tt.fileType, tt.owner, got, tt.want) + } + }) + } +} + +func TestMapPublicKeys(t *testing.T) { + t.Run("nil input yields nil", func(t *testing.T) { + if got := mapPublicKeys(nil); got != nil { + t.Errorf("mapPublicKeys(nil) = %v, want nil", got) + } + }) + + t.Run("maps label and key preserving order", func(t *testing.T) { + in := []roverv1.PublicKey{ + {Label: "provider-key", Key: "ssh-ed25519 AAAA"}, + {Label: "consumer-key", Key: "ssh-ed25519 BBBB"}, + } + got := mapPublicKeys(in) + if len(got) != 2 { + t.Fatalf("mapPublicKeys len = %d, want 2", len(got)) + } + if got[0].Label != "provider-key" || got[0].Key != "ssh-ed25519 AAAA" { + t.Errorf("got[0] = %+v", got[0]) + } + if got[1].Label != "consumer-key" || got[1].Key != "ssh-ed25519 BBBB" { + t.Errorf("got[1] = %+v", got[1]) + } + }) +} diff --git a/rover/internal/handler/rover/handler.go b/rover/internal/handler/rover/handler.go index 6e47a8571..5f8032a12 100644 --- a/rover/internal/handler/rover/handler.go +++ b/rover/internal/handler/rover/handler.go @@ -19,11 +19,13 @@ import ( "github.com/telekom/controlplane/common/pkg/types" "github.com/telekom/controlplane/common/pkg/util/contextutil" eventv1 "github.com/telekom/controlplane/event/api/v1" + filev1 "github.com/telekom/controlplane/file/api/v1" permissionv1 "github.com/telekom/controlplane/permission/api/v1" roverv1 "github.com/telekom/controlplane/rover/api/v1" "github.com/telekom/controlplane/rover/internal/handler/rover/api" "github.com/telekom/controlplane/rover/internal/handler/rover/application" "github.com/telekom/controlplane/rover/internal/handler/rover/event" + "github.com/telekom/controlplane/rover/internal/handler/rover/file" "github.com/telekom/controlplane/rover/internal/handler/rover/permission" secretsapi "github.com/telekom/controlplane/secret-manager/api" ) @@ -44,6 +46,10 @@ func (h *RoverHandler) CreateOrUpdate(ctx context.Context, roverObj *roverv1.Rov if config.FeaturePermission.IsEnabled() { c.AddKnownTypeToState(&permissionv1.PermissionSet{}) } + if config.FeatureFile.IsEnabled() { + c.AddKnownTypeToState(&filev1.FileExposure{}) + c.AddKnownTypeToState(&filev1.FileSubscription{}) + } // Create Application from Rover err := application.HandleApplication(ctx, c, roverObj) @@ -54,6 +60,7 @@ func (h *RoverHandler) CreateOrUpdate(ctx context.Context, roverObj *roverv1.Rov // Handle exposures roverObj.Status.ApiExposures = make([]types.ObjectRef, 0, len(roverObj.Spec.Exposures)) roverObj.Status.EventExposures = make([]types.ObjectRef, 0, len(roverObj.Spec.Exposures)) + roverObj.Status.FileExposures = make([]types.ObjectRef, 0, len(roverObj.Spec.Exposures)) seenDescriminators := make(map[string]struct{}) for _, exp := range roverObj.Spec.Exposures { @@ -82,6 +89,16 @@ func (h *RoverHandler) CreateOrUpdate(ctx context.Context, roverObj *roverv1.Rov return errors.Wrap(err, "failed to handle event exposure") } + case roverv1.TypeFile: + // Duplicate file types are rejected by the Rover admission webhook + if !config.FeatureFile.IsEnabled() { + log.Info("file exposure skipped, feature has not been enabled") + continue + } + if err := file.HandleExposure(ctx, c, roverObj, exp.File); err != nil { + return errors.Wrap(err, "failed to handle file exposure") + } + default: return errors.New("unknown exposure type: " + exp.Type().String()) } @@ -90,6 +107,7 @@ func (h *RoverHandler) CreateOrUpdate(ctx context.Context, roverObj *roverv1.Rov // Handle subscriptions roverObj.Status.ApiSubscriptions = make([]types.ObjectRef, 0, len(roverObj.Spec.Subscriptions)) roverObj.Status.EventSubscriptions = make([]types.ObjectRef, 0, len(roverObj.Spec.Subscriptions)) + roverObj.Status.FileSubscriptions = make([]types.ObjectRef, 0, len(roverObj.Spec.Subscriptions)) for _, sub := range roverObj.Spec.Subscriptions { switch sub.Type() { case roverv1.TypeApi: @@ -108,6 +126,15 @@ func (h *RoverHandler) CreateOrUpdate(ctx context.Context, roverObj *roverv1.Rov return errors.Wrap(err, "failed to handle event subscription") } + case roverv1.TypeFile: + if !config.FeatureFile.IsEnabled() { + log.Info("file subscription skipped, feature has not been enabled") + continue + } + if err := file.HandleSubscription(ctx, c, roverObj, sub.File); err != nil { + return errors.Wrap(err, "failed to handle file subscription") + } + default: return errors.New("unknown subscription type: " + sub.Type().String()) } From cfc834391f51fd778e08153ba9ce0db52e400c0b Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Mon, 6 Jul 2026 14:00:33 +0300 Subject: [PATCH 09/31] feat(dhei-20905): fix comments of PR add / change file/rover api add feature config, handler controller wh for file FileSpecification in progress --- file/api/go.sum.license | 3 + file/api/v1/file_shared_types.go | 15 + file/api/v1/fileexposure_types.go | 21 +- file/api/v1/filesubscription_types.go | 11 +- file/api/v1/filetype_types.go | 41 +- file/api/v1/suite_test.go | 17 + file/api/v1/types_test.go | 105 ++++++ file/api/v1/zz_generated.deepcopy.go | 357 ++++++++++++++++++ .../file.ei.telekom.de_fileexposures.yaml | 62 +-- .../file.ei.telekom.de_filesubscriptions.yaml | 33 +- .../bases/file.ei.telekom.de_filetypes.yaml | 56 ++- rover/api/v1/filespecification_types.go | 116 ++++++ rover/api/v1/roadmap_types_test.go | 1 + rover/api/v1/rover_types.go | 129 ++++++- rover/api/v1/rover_types_test.go | 126 ++++++- rover/api/v1/zz_generated.deepcopy.go | 176 +++++++++ rover/cmd/main.go | 14 + ...r.cp.ei.telekom.de_filespecifications.yaml | 152 ++++++++ .../bases/rover.cp.ei.telekom.de_rovers.yaml | 166 +++++++- rover/config/crd/kustomization.yaml | 1 + rover/config/rbac/role.yaml | 17 + rover/config/webhook/manifests.yaml | 20 + rover/go.mod | 2 + .../filespecification_controller.go | 57 +++ rover/internal/controller/rover_controller.go | 6 + .../handler/filespecification/handler.go | 78 ++++ .../handler/rover/application/application.go | 23 +- rover/internal/handler/rover/file/exposure.go | 21 +- .../handler/rover/file/subscription.go | 9 +- .../webhook/v1/filespecification_webhook.go | 13 +- .../v1/filespecification_webhook_test.go | 63 +++- 31 files changed, 1753 insertions(+), 158 deletions(-) create mode 100644 file/api/go.sum.license create mode 100644 file/api/v1/suite_test.go create mode 100644 file/api/v1/types_test.go create mode 100644 file/api/v1/zz_generated.deepcopy.go create mode 100644 rover/api/v1/filespecification_types.go create mode 100644 rover/config/crd/bases/rover.cp.ei.telekom.de_filespecifications.yaml create mode 100644 rover/internal/controller/filespecification_controller.go create mode 100644 rover/internal/handler/filespecification/handler.go diff --git a/file/api/go.sum.license b/file/api/go.sum.license new file mode 100644 index 000000000..be863cd5c --- /dev/null +++ b/file/api/go.sum.license @@ -0,0 +1,3 @@ +Copyright 2026 Deutsche Telekom IT GmbH + +SPDX-License-Identifier: Apache-2.0 diff --git a/file/api/v1/file_shared_types.go b/file/api/v1/file_shared_types.go index dd26ba352..72dcf735e 100644 --- a/file/api/v1/file_shared_types.go +++ b/file/api/v1/file_shared_types.go @@ -44,3 +44,18 @@ const ( func (a ApprovalStrategy) String() string { return string(a) } + +// Approval configures how subscriptions to a file type exposure are approved. +type Approval struct { + // Strategy defines the approval mode for subscriptions to this file type. + // +kubebuilder:validation:Enum=Auto;Simple;FourEyes + // +kubebuilder:default=Simple + Strategy ApprovalStrategy `json:"strategy"` + + // TrustedTeams identifies teams that are trusted for approving subscriptions + // to this file type. Per default your own team is trusted. + // +kubebuilder:validation:Optional + // +kubebuilder:validation:MinItems=0 + // +kubebuilder:validation:MaxItems=10 + TrustedTeams []string `json:"trustedTeams,omitempty"` +} diff --git a/file/api/v1/fileexposure_types.go b/file/api/v1/fileexposure_types.go index 1d4f22a72..764b389f1 100644 --- a/file/api/v1/fileexposure_types.go +++ b/file/api/v1/fileexposure_types.go @@ -11,31 +11,29 @@ import ( ) // FileExposureSpec defines the desired state of FileExposure. -// It is created in the file domain from a rover-domain Rover exposure (1:1). The -// derived logical Application is created without an Identity client. type FileExposureSpec struct { // Approval configures how subscriptions to this file type are approved. - // +kubebuilder:default=Simple - Approval ApprovalStrategy `json:"approval,omitempty"` + Approval Approval `json:"approval"` // Visibility defines who can see and subscribe to this file type. // +kubebuilder:default=Enterprise Visibility Visibility `json:"visibility,omitempty"` - // FileTypeRef references the file-domain FileType this exposure belongs to. - // It must live in the same namespace as this FileExposure. + // FileType is the file type identifier this exposure belongs to. + // References the FileType CR via MakeFileTypeName() conversion. // +kubebuilder:validation:Required - FileTypeRef ctypes.ObjectRef `json:"fileTypeRef"` + // +kubebuilder:validation:MinLength=1 + FileType string `json:"fileType"` // PublicKeys are the SSH public keys registered for the provider's SFTP user. // +kubebuilder:validation:Required // +kubebuilder:validation:MinItems=1 PublicKeys []PublicKey `json:"publicKeys"` - // ZoneServiceConfigRef references the zone-scoped service configuration - // (provided by the SFTP/DDS domain) used to reach the backend for this exposure. - // +optional - ZoneServiceConfigRef *ctypes.ObjectRef `json:"zoneServiceConfigRef,omitempty"` + // Zone references the Zone CR where this file type is exposed. + // On this layer only the Zone ref is passed; the file domain resolves it to + // the zone-scoped service configuration for the backend. + Zone ctypes.ObjectRef `json:"zone"` } // FileExposureStatus defines the observed state of FileExposure. @@ -57,6 +55,7 @@ type FileExposureStatus struct { // +kubebuilder:object:root=true // +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="FileType",type="string",JSONPath=".spec.fileType",description="The file type identifier" // +kubebuilder:printcolumn:name="Active",type="boolean",JSONPath=".status.active",description="Whether this exposure is provisioned" // +kubebuilder:printcolumn:name="CreatedAt",type="date",JSONPath=".metadata.creationTimestamp",description="Creation timestamp" diff --git a/file/api/v1/filesubscription_types.go b/file/api/v1/filesubscription_types.go index 7ed2b595b..21a17d87c 100644 --- a/file/api/v1/filesubscription_types.go +++ b/file/api/v1/filesubscription_types.go @@ -13,9 +13,11 @@ import ( // FileSubscriptionSpec defines the desired state of FileSubscription. // It is created in the file domain from a rover-domain Rover subscription (1:1). type FileSubscriptionSpec struct { - // FileTypeRef references the file-domain FileType this subscription belongs to. + // FileType is the file type identifier this subscription belongs to. + // References the FileType CR via MakeFileTypeName() conversion. // +kubebuilder:validation:Required - FileTypeRef ctypes.ObjectRef `json:"fileTypeRef"` + // +kubebuilder:validation:MinLength=1 + FileType string `json:"fileType"` // PublicKeys are the SSH public keys registered for the consumer's SFTP user. // +kubebuilder:validation:Required @@ -31,14 +33,11 @@ type FileSubscriptionStatus struct { // +patchMergeKey=type // +optional Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` - - // Active indicates whether this subscription has been provisioned. - Active bool `json:"active,omitempty"` } // +kubebuilder:object:root=true // +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="Active",type="boolean",JSONPath=".status.active",description="Whether this subscription is provisioned" +// +kubebuilder:printcolumn:name="FileType",type="string",JSONPath=".spec.fileType",description="The file type identifier" // +kubebuilder:printcolumn:name="CreatedAt",type="date",JSONPath=".metadata.creationTimestamp",description="Creation timestamp" // FileSubscription is the Schema for the filesubscriptions API. diff --git a/file/api/v1/filetype_types.go b/file/api/v1/filetype_types.go index 976f399b4..1eb98386a 100644 --- a/file/api/v1/filetype_types.go +++ b/file/api/v1/filetype_types.go @@ -5,29 +5,41 @@ package v1 import ( + "strings" + + "github.com/telekom/controlplane/common/pkg/config" ctypes "github.com/telekom/controlplane/common/pkg/types" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// FileTypeLabelKey is the label used to associate FileExposure/FileSubscription +// resources with their FileType +var FileTypeLabelKey = config.BuildLabelKey("filetype") + +// MakeFileTypeName generates a Kubernetes resource name from a file type identifier. +func MakeFileTypeName(fileType string) string { + return strings.ToLower(strings.ReplaceAll(fileType, ".", "-")) +} + // FileTypeSpec defines the desired state of FileType. -// A FileType is the file-domain registry entry for a file type. It is created in the -// file domain from a rover-domain FileSpecification (1:1) and is the canonical -// resource that FileExposure (1:1) and FileSubscription (1:n) reference via their -// fileTypeRef (mirrors event.EventType). type FileTypeSpec struct { + // Type is the dot-separated file type identifier (e.g. "de.telekom.eni.invoices.v1"). + // Used to generate the resource name via MakeFileTypeName() conversion. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^[a-z0-9]+(\.[a-z0-9]+)*$` + Type string `json:"type"` + // Description provides a human-readable summary of this file type. // +optional Description string `json:"description,omitempty"` - // ExposureRef references the file-domain FileExposure created for this file type (1:1). - // +optional - ExposureRef *ctypes.ObjectRef `json:"exposureRef,omitempty"` - - // SubscriptionRefs references the file-domain FileSubscriptions created for this - // file type (1:n). + // Specification contains the file ID reference from the file manager for the + // optional document that describes this file type. // +optional - SubscriptionRefs []ctypes.ObjectRef `json:"subscriptionRefs,omitempty"` + Specification string `json:"specification,omitempty"` } // FileTypeStatus defines the observed state of FileType. @@ -39,14 +51,17 @@ type FileTypeStatus struct { // +optional Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` - // Active indicates whether this FileType has been provisioned. + // Active indicates whether this FileType is the active singleton for its file + // type identifier. When multiple FileTypes exist for the same identifier, only + // the oldest non-deleted one is active. Active bool `json:"active,omitempty"` } // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:shortName=ftype -// +kubebuilder:printcolumn:name="Active",type="boolean",JSONPath=".status.active",description="Whether this file type is provisioned" +// +kubebuilder:printcolumn:name="Type",type="string",JSONPath=".spec.type",description="The file type identifier" +// +kubebuilder:printcolumn:name="Active",type="boolean",JSONPath=".status.active",description="Indicates if this FileType is the active singleton" // +kubebuilder:printcolumn:name="CreatedAt",type="date",JSONPath=".metadata.creationTimestamp",description="Creation timestamp" // FileType is the Schema for the filetypes API. diff --git a/file/api/v1/suite_test.go b/file/api/v1/suite_test.go new file mode 100644 index 000000000..8a6ab691d --- /dev/null +++ b/file/api/v1/suite_test.go @@ -0,0 +1,17 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package v1_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestFileApiV1(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "File API V1 Suite") +} diff --git a/file/api/v1/types_test.go b/file/api/v1/types_test.go new file mode 100644 index 000000000..f78dbc91a --- /dev/null +++ b/file/api/v1/types_test.go @@ -0,0 +1,105 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package v1_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + ctypes "github.com/telekom/controlplane/common/pkg/types" + v1 "github.com/telekom/controlplane/file/api/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("MakeFileTypeName", func() { + DescribeTable("converts file type strings to Kubernetes resource names", + func(input, expected string) { + Expect(v1.MakeFileTypeName(input)).To(Equal(expected)) + }, + Entry("file type with dots", "de.telekom.eni.foo.v1", "de-telekom-eni-foo-v1"), + Entry("already hyphenated", "de-telekom-eni-foo-v1", "de-telekom-eni-foo-v1"), + Entry("empty string", "", ""), + Entry("mixed case with dots", "De.Telekom.V1", "de-telekom-v1"), + ) +}) + +var _ = Describe("FileType", func() { + It("gets and sets conditions", func() { + ft := &v1.FileType{ + ObjectMeta: metav1.ObjectMeta{Name: "demo-v1", Namespace: "team-ns"}, + Spec: v1.FileTypeSpec{Type: "demo-v1", Description: "demo"}, + } + Expect(ft.GetConditions()).To(BeEmpty()) + changed := ft.SetCondition(metav1.Condition{ + Type: "Ready", + Status: metav1.ConditionTrue, + Reason: "Provisioned", + }) + Expect(changed).To(BeTrue()) + Expect(ft.GetConditions()).To(HaveLen(1)) + Expect(ft.GetConditions()[0].Type).To(Equal("Ready")) + }) + + It("exposes list items via GetItems", func() { + list := &v1.FileTypeList{Items: []v1.FileType{ + {ObjectMeta: metav1.ObjectMeta{Name: "a"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "b"}}, + }} + items := list.GetItems() + Expect(items).To(HaveLen(2)) + Expect(items[0].GetName()).To(Equal("a")) + Expect(items[1].GetName()).To(Equal("b")) + }) +}) + +var _ = Describe("FileExposure", func() { + It("deep-copies spec and status without aliasing", func() { + orig := &v1.FileExposure{ + ObjectMeta: metav1.ObjectMeta{Name: "foo-v1--provider", Namespace: "team-ns"}, + Spec: v1.FileExposureSpec{ + Approval: v1.Approval{Strategy: v1.ApprovalStrategySimple}, + Visibility: v1.VisibilityEnterprise, + FileType: "foo-v1", + PublicKeys: []v1.PublicKey{ + {Label: "provider-key", Key: "ssh-ed25519 AAAA"}, + }, + }, + Status: v1.FileExposureStatus{ + Active: true, + Subscriptions: []ctypes.ObjectRef{{Name: "sub", Namespace: "team-ns"}}, + }, + } + + clone := orig.DeepCopy() + Expect(clone).To(Equal(orig)) + + // Mutating the clone must not affect the original. + clone.Spec.PublicKeys[0].Key = "changed" + clone.Status.Subscriptions[0].Name = "other" + Expect(orig.Spec.PublicKeys[0].Key).To(Equal("ssh-ed25519 AAAA")) + Expect(orig.Status.Subscriptions[0].Name).To(Equal("sub")) + }) + + It("gets and sets conditions", func() { + exp := &v1.FileExposure{} + Expect(exp.SetCondition(metav1.Condition{Type: "Ready", Status: metav1.ConditionFalse, Reason: "Pending"})).To(BeTrue()) + Expect(exp.GetConditions()).To(HaveLen(1)) + }) +}) + +var _ = Describe("FileSubscription", func() { + It("deep-copies public keys without aliasing", func() { + orig := &v1.FileSubscription{ + Spec: v1.FileSubscriptionSpec{ + FileType: "foo-v1", + PublicKeys: []v1.PublicKey{{Label: "consumer-key", Key: "ssh-ed25519 BBBB"}}, + }, + } + clone := orig.DeepCopy() + Expect(clone).To(Equal(orig)) + clone.Spec.PublicKeys[0].Label = "changed" + Expect(orig.Spec.PublicKeys[0].Label).To(Equal("consumer-key")) + }) +}) diff --git a/file/api/v1/zz_generated.deepcopy.go b/file/api/v1/zz_generated.deepcopy.go new file mode 100644 index 000000000..2cf2f20dc --- /dev/null +++ b/file/api/v1/zz_generated.deepcopy.go @@ -0,0 +1,357 @@ +//go:build !ignore_autogenerated + +// SPDX-FileCopyrightText: 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +import ( + "github.com/telekom/controlplane/common/pkg/types" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Approval) DeepCopyInto(out *Approval) { + *out = *in + if in.TrustedTeams != nil { + in, out := &in.TrustedTeams, &out.TrustedTeams + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Approval. +func (in *Approval) DeepCopy() *Approval { + if in == nil { + return nil + } + out := new(Approval) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PublicKey) DeepCopyInto(out *PublicKey) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicKey. +func (in *PublicKey) DeepCopy() *PublicKey { + if in == nil { + return nil + } + out := new(PublicKey) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileTypeSpec) DeepCopyInto(out *FileTypeSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileTypeSpec. +func (in *FileTypeSpec) DeepCopy() *FileTypeSpec { + if in == nil { + return nil + } + out := new(FileTypeSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileTypeStatus) DeepCopyInto(out *FileTypeStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileTypeStatus. +func (in *FileTypeStatus) DeepCopy() *FileTypeStatus { + if in == nil { + return nil + } + out := new(FileTypeStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileType) DeepCopyInto(out *FileType) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileType. +func (in *FileType) DeepCopy() *FileType { + if in == nil { + return nil + } + out := new(FileType) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FileType) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileTypeList) DeepCopyInto(out *FileTypeList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]FileType, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileTypeList. +func (in *FileTypeList) DeepCopy() *FileTypeList { + if in == nil { + return nil + } + out := new(FileTypeList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FileTypeList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileExposureSpec) DeepCopyInto(out *FileExposureSpec) { + *out = *in + in.Approval.DeepCopyInto(&out.Approval) + if in.PublicKeys != nil { + in, out := &in.PublicKeys, &out.PublicKeys + *out = make([]PublicKey, len(*in)) + copy(*out, *in) + } + in.Zone.DeepCopyInto(&out.Zone) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileExposureSpec. +func (in *FileExposureSpec) DeepCopy() *FileExposureSpec { + if in == nil { + return nil + } + out := new(FileExposureSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileExposureStatus) DeepCopyInto(out *FileExposureStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Subscriptions != nil { + in, out := &in.Subscriptions, &out.Subscriptions + *out = make([]types.ObjectRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileExposureStatus. +func (in *FileExposureStatus) DeepCopy() *FileExposureStatus { + if in == nil { + return nil + } + out := new(FileExposureStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileExposure) DeepCopyInto(out *FileExposure) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileExposure. +func (in *FileExposure) DeepCopy() *FileExposure { + if in == nil { + return nil + } + out := new(FileExposure) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FileExposure) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileExposureList) DeepCopyInto(out *FileExposureList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]FileExposure, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileExposureList. +func (in *FileExposureList) DeepCopy() *FileExposureList { + if in == nil { + return nil + } + out := new(FileExposureList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FileExposureList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileSubscriptionSpec) DeepCopyInto(out *FileSubscriptionSpec) { + *out = *in + if in.PublicKeys != nil { + in, out := &in.PublicKeys, &out.PublicKeys + *out = make([]PublicKey, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSubscriptionSpec. +func (in *FileSubscriptionSpec) DeepCopy() *FileSubscriptionSpec { + if in == nil { + return nil + } + out := new(FileSubscriptionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileSubscriptionStatus) DeepCopyInto(out *FileSubscriptionStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSubscriptionStatus. +func (in *FileSubscriptionStatus) DeepCopy() *FileSubscriptionStatus { + if in == nil { + return nil + } + out := new(FileSubscriptionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileSubscription) DeepCopyInto(out *FileSubscription) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSubscription. +func (in *FileSubscription) DeepCopy() *FileSubscription { + if in == nil { + return nil + } + out := new(FileSubscription) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FileSubscription) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileSubscriptionList) DeepCopyInto(out *FileSubscriptionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]FileSubscription, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSubscriptionList. +func (in *FileSubscriptionList) DeepCopy() *FileSubscriptionList { + if in == nil { + return nil + } + out := new(FileSubscriptionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FileSubscriptionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml b/file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml index fe4579da8..6e8d63c6a 100644 --- a/file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml +++ b/file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml @@ -18,6 +18,10 @@ spec: scope: Namespaced versions: - additionalPrinterColumns: + - description: The file type identifier + jsonPath: .spec.fileType + name: FileType + type: string - description: Whether this exposure is provisioned jsonPath: .status.active name: Active @@ -44,13 +48,35 @@ spec: description: ExposureSpec defines the desired state of a file Exposure. properties: approval: - default: Simple description: Approval configures how subscriptions to this file type are approved. - enum: - - Auto - - Simple - - FourEyes + properties: + strategy: + default: Simple + description: Strategy defines the approval mode for subscriptions + to this file type. + enum: + - Auto + - Simple + - FourEyes + type: string + trustedTeams: + description: |- + TrustedTeams identifies teams that are trusted for approving subscriptions + to this file type. Per default your own team is trusted. + items: + type: string + maxItems: 10 + minItems: 0 + type: array + required: + - strategy + type: object + fileType: + description: |- + FileType is the file type identifier this exposure belongs to. + References the FileType CR via MakeFileTypeName() conversion. + minLength: 1 type: string publicKeys: description: PublicKeys are the SSH public keys registered for the @@ -75,21 +101,6 @@ spec: type: object minItems: 1 type: array - fileTypeRef: - description: |- - FileTypeRef references the file-domain FileType this exposure belongs to. - It must live in the same namespace as this FileExposure. - properties: - name: - type: string - namespace: - type: string - uid: - type: string - required: - - name - - namespace - type: object visibility: default: Enterprise description: Visibility defines who can see and subscribe to this file @@ -99,10 +110,11 @@ spec: - Zone - Enterprise type: string - zoneServiceConfigRef: + zone: description: |- - ZoneServiceConfigRef references the zone-scoped service configuration - (provided by the SFTP/DDS domain) used to reach the backend for this exposure. + Zone references the Zone CR where this file type is exposed. + On this layer only the Zone ref is passed; the file domain resolves it to + the zone-scoped service configuration for the backend. properties: name: type: string @@ -115,8 +127,10 @@ spec: - namespace type: object required: + - approval + - fileType - publicKeys - - specRef + - zone type: object status: description: ExposureStatus defines the observed state of a file Exposure. diff --git a/file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml b/file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml index dffe37be6..1ff07fddc 100644 --- a/file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml +++ b/file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml @@ -18,10 +18,10 @@ spec: scope: Namespaced versions: - additionalPrinterColumns: - - description: Whether this subscription is provisioned - jsonPath: .status.active - name: Active - type: boolean + - description: The file type identifier + jsonPath: .spec.fileType + name: FileType + type: string - description: Creation timestamp jsonPath: .metadata.creationTimestamp name: CreatedAt @@ -42,20 +42,12 @@ spec: spec: description: FileSubscriptionSpec defines the desired state of FileSubscription. properties: - fileTypeRef: - description: FileTypeRef references the file-domain FileType this subscription - belongs to. - properties: - name: - type: string - namespace: - type: string - uid: - type: string - required: - - name - - namespace - type: object + fileType: + description: |- + FileType is the file type identifier this subscription belongs to. + References the FileType CR via MakeFileTypeName() conversion. + minLength: 1 + type: string publicKeys: description: PublicKeys are the SSH public keys registered for the consumer's SFTP user. @@ -80,15 +72,12 @@ spec: minItems: 1 type: array required: - - fileTypeRef + - fileType - publicKeys type: object status: description: FileSubscriptionStatus defines the observed state of FileSubscription. properties: - active: - description: Active indicates whether this subscription has been provisioned. - type: boolean conditions: items: description: Condition contains details for one aspect of the current diff --git a/file/config/crd/bases/file.ei.telekom.de_filetypes.yaml b/file/config/crd/bases/file.ei.telekom.de_filetypes.yaml index 47fbd05e0..4bc7a03e7 100644 --- a/file/config/crd/bases/file.ei.telekom.de_filetypes.yaml +++ b/file/config/crd/bases/file.ei.telekom.de_filetypes.yaml @@ -20,7 +20,11 @@ spec: scope: Namespaced versions: - additionalPrinterColumns: - - description: Whether this file spec is provisioned + - description: The file type identifier + jsonPath: .spec.type + name: Type + type: string + - description: Indicates if this FileType is the active singleton jsonPath: .status.active name: Active type: boolean @@ -49,42 +53,30 @@ spec: description: Description provides a human-readable summary of this file type. type: string - exposureRef: - description: ExposureRef references the file-domain FileExposure created - for this file type (1:1). - properties: - name: - type: string - namespace: - type: string - uid: - type: string - required: - - name - - namespace - type: object - subscriptionRefs: - description: SubscriptionRefs references the file-domain FileSubscriptions - created for this file type (1:n). - items: - properties: - name: - type: string - namespace: - type: string - uid: - type: string - required: - - name - - namespace - type: object - type: array + specification: + description: |- + Specification contains the file ID reference from the file manager for the + optional document that describes this file type. + type: string + type: + description: |- + Type is the dot-separated file type identifier (e.g. "de.telekom.eni.invoices.v1"). + Used to generate the resource name via MakeFileTypeName() conversion. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]+(\.[a-z0-9]+)*$ + type: string + required: + - type type: object status: description: FileTypeStatus defines the observed state of FileType. properties: active: - description: Active indicates whether this FileType has been provisioned. + description: |- + Active indicates whether this FileType is the active singleton for its file + type identifier. When multiple FileTypes exist for the same identifier, only + the oldest non-deleted one is active. type: boolean conditions: items: diff --git a/rover/api/v1/filespecification_types.go b/rover/api/v1/filespecification_types.go new file mode 100644 index 000000000..10cf715e4 --- /dev/null +++ b/rover/api/v1/filespecification_types.go @@ -0,0 +1,116 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +import ( + "strings" + + "github.com/telekom/controlplane/common/pkg/types" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// MakeFileSpecificationName generates a name for the FileType resource based on +// the file type identifier of the FileSpecification (its metadata name). +// MakeEventSpecificationName / MakeName (api). +func MakeFileSpecificationName(fileSpec *FileSpecification) string { + return strings.ToLower(strings.ReplaceAll(fileSpec.Name, ".", "-")) +} + +// FileStorageType selects the file-transfer backend used to store/exchange files +// for a file type. Currently only SFTP is supported +// +kubebuilder:validation:Enum=sftp +type FileStorageType string + +const ( + // FileStorageTypeSFTP indicates the file type is handled via the SFTP backend + FileStorageTypeSFTP FileStorageType = "sftp" +) + +func (t FileStorageType) String() string { + return string(t) +} + +// FileSpecificationSpec defines the desired state of FileSpecification. +// It mirrors the internal Rover-domain form from spec_dcp: only description and the +// backend selector are stored; the file type identifier lives in metadata.name. +type FileSpecificationSpec struct { + // Description provides a human-readable summary of this file type. + // +optional + Description string `json:"description,omitempty"` + + // Specification contains the file ID reference from the file manager for the + // optional document that describes this file type. + // +optional + Specification string `json:"specification,omitempty"` + + // StorageType selects the file-transfer backend. + // +kubebuilder:validation:Optional + // +kubebuilder:default=sftp + StorageType FileStorageType `json:"storageType,omitempty"` +} + +// FileSpecificationStatus defines the observed state of FileSpecification. +type FileSpecificationStatus struct { + // +listType=map + // +listMapKey=type + // +patchStrategy=merge + // +patchMergeKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` + + // FileType references the file-domain FileType created from this specification. + // It is populated by the FileSpecification reconciler + // (rover/internal/controller/filespecification_controller.go), mirroring how + // ApiSpecification creates Api and EventSpecification creates EventType. + FileType types.ObjectRef `json:"fileType,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// FileSpecification is the Schema for the filespecifications API. +// It defines a file type's metadata and creates the corresponding file-domain +// FileType, analogous to how ApiSpecification creates Api resources and +// EventSpecification creates EventType resources. +type FileSpecification struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FileSpecificationSpec `json:"spec,omitempty"` + Status FileSpecificationStatus `json:"status,omitempty"` +} + +var _ types.Object = &FileSpecification{} + +func (r *FileSpecification) GetConditions() []metav1.Condition { + return r.Status.Conditions +} + +func (r *FileSpecification) SetCondition(condition metav1.Condition) bool { + return meta.SetStatusCondition(&r.Status.Conditions, condition) +} + +// +kubebuilder:object:root=true + +type FileSpecificationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []FileSpecification `json:"items"` +} + +var _ types.ObjectList = &FileSpecificationList{} + +func (r *FileSpecificationList) GetItems() []types.Object { + items := make([]types.Object, len(r.Items)) + for i := range r.Items { + items[i] = &r.Items[i] + } + return items +} + +func init() { + SchemeBuilder.Register(&FileSpecification{}, &FileSpecificationList{}) +} diff --git a/rover/api/v1/roadmap_types_test.go b/rover/api/v1/roadmap_types_test.go index 93d14b0ee..d61dea3b3 100644 --- a/rover/api/v1/roadmap_types_test.go +++ b/rover/api/v1/roadmap_types_test.go @@ -7,6 +7,7 @@ package v1_test import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" "github.com/telekom/controlplane/common/pkg/types" v1 "github.com/telekom/controlplane/rover/api/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" diff --git a/rover/api/v1/rover_types.go b/rover/api/v1/rover_types.go index 0756d78d9..0ccf00ccd 100644 --- a/rover/api/v1/rover_types.go +++ b/rover/api/v1/rover_types.go @@ -5,6 +5,8 @@ package v1 import ( + "slices" + "github.com/telekom/controlplane/common/pkg/types" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -30,6 +32,18 @@ type RoverStatus struct { EventExposures []types.ObjectRef `json:"eventExposures,omitempty"` // EventSubscriptions are references to EventSubscription resources created by this Rover EventSubscriptions []types.ObjectRef `json:"eventSubscriptions,omitempty"` + // FileExposures are references to FileExposure resources created by this Rover in the file domain. + // + // TODO(DHEI-20903): today RoverHandler.CreateOrUpdate only initialises this slice + // (make(..., 0)); it is populated (append of the created file-domain resource refs) + // by the file handler dispatch once the file domain module is available. + // Populated from: rover/internal/handler/rover/handler.go, case roverv1.TypeFile. + FileExposures []types.ObjectRef `json:"fileExposures,omitempty"` + // FileSubscriptions are references to FileSubscription resources created by this Rover in the file domain. + // + // TODO(DHEI-20903): see FileExposures — populated by the file handler dispatch + // (rover/internal/handler/rover/handler.go, case roverv1.TypeFile) once delivered. + FileSubscriptions []types.ObjectRef `json:"fileSubscriptions,omitempty"` // PermissionSets are references to PermissionSet resources created by this Rover PermissionSets []types.ObjectRef `json:"permissionSets,omitempty"` } @@ -175,8 +189,39 @@ const ( TypeApi Type = "api" // TypeEvent represents an Event type resource TypeEvent Type = "event" + // TypeFile represents a File type resource (SFTP integration) + TypeFile Type = "file" +) + +// FileVariant selects the file-transfer backend used for a file type exposure or +// subscription. Currently only SFTP is supported, as CloudWalker has been retired. +type FileVariant string + +// String returns the raw string value of the FileVariant. +// +// TODO(DHEI-20903): currently unused in production code (only asserted in tests). +// It will be called by the file-domain handler +// (rover/internal/handler/rover/file, added in DHEI-20903) when logging/serialising +// the selected variant while creating the file-domain CRD. +func (v FileVariant) String() string { + return string(v) +} + +const ( + // FileVariantSFTP indicates that the file type is handled via the SFTP backend. + FileVariantSFTP FileVariant = "sftp" ) +// SupportedFileTypeZones lists the zones on which file type (SFTP) exposures and +// subscriptions are currently supported. CloudWalker has been retired, so SFTP file +// types are limited to the external zones cetus and canis. +var SupportedFileTypeZones = []string{"cetus", "canis"} + +// IsFileTypeZoneSupported reports whether the given zone supports file types (SFTP). +func IsFileTypeZoneSupported(zone string) bool { + return slices.Contains(SupportedFileTypeZones, zone) +} + // ApprovalStrategy defines the approval workflow for API exposure type ApprovalStrategy string @@ -224,8 +269,8 @@ type RoverM2MAuthentication struct { } // Exposure defines a service that is exposed by this Rover -// +kubebuilder:validation:XValidation:rule="self == null || has(self.api) || has(self.event)", message="At least one of api or event must be specified" -// +kubebuilder:validation:XValidation:rule="self == null || (!has(self.api) && has(self.event)) || (has(self.api) && !has(self.event))", message="Only one of api or event can be specified (XOR relationship)" +// +kubebuilder:validation:XValidation:rule="self == null || has(self.api) || has(self.event) || has(self.file)", message="At least one of api, event or file must be specified" +// +kubebuilder:validation:XValidation:rule="self == null || [has(self.api), has(self.event), has(self.file)].filter(x, x).size() == 1", message="Only one of api, event or file can be specified (XOR relationship)" type Exposure struct { // Api defines an API-based service exposure configuration // +kubebuilder:validation:Optional @@ -233,6 +278,9 @@ type Exposure struct { // Event defines an Event-based service exposure configuration // +kubebuilder:validation:Optional Event *EventExposure `json:"event,omitempty"` + // File defines a File-based (SFTP) service exposure configuration + // +kubebuilder:validation:Optional + File *FileExposure `json:"file,omitempty"` } func (e *Exposure) Type() Type { @@ -242,12 +290,15 @@ func (e *Exposure) Type() Type { if e.Event != nil { return TypeEvent } + if e.File != nil { + return TypeFile + } return "" } // Subscription defines a service that this Rover consumes -// +kubebuilder:validation:XValidation:rule="self == null || has(self.api) || has(self.event)", message="At least one of api or event must be specified" -// +kubebuilder:validation:XValidation:rule="(has(self.api) && !has(self.event)) || (!has(self.api) && has(self.event))", message="Only one of api or event can be specified (XOR relationship)" +// +kubebuilder:validation:XValidation:rule="self == null || has(self.api) || has(self.event) || has(self.file)", message="At least one of api, event or file must be specified" +// +kubebuilder:validation:XValidation:rule="self == null || [has(self.api), has(self.event), has(self.file)].filter(x, x).size() == 1", message="Only one of api, event or file can be specified (XOR relationship)" type Subscription struct { // Api defines an API-based service subscription configuration // +kubebuilder:validation:Optional @@ -255,6 +306,9 @@ type Subscription struct { // Event defines an Event-based service subscription configuration // +kubebuilder:validation:Optional Event *EventSubscription `json:"event,omitempty"` + // File defines a File-based (SFTP) service subscription configuration + // +kubebuilder:validation:Optional + File *FileSubscription `json:"file,omitempty"` } func (s *Subscription) Type() Type { @@ -264,6 +318,9 @@ func (s *Subscription) Type() Type { if s.Event != nil { return TypeEvent } + if s.File != nil { + return TypeFile + } return "" } @@ -390,6 +447,70 @@ type EventSubscription struct { Scopes []string `json:"scopes,omitempty"` } +// FileExposure defines a file type that is exposed by this Rover via SFTP. +// Applying it registers the provider's SSH public keys on the corresponding +// SFTP user (shared space) created from the matching FileSpecification. +type FileExposure struct { + // FileType identifies the file type that is exposed. It must match the + // name (and spec.type) of an applied FileSpecification. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + FileType string `json:"fileType"` + + // Variant selects the file-transfer backend. Currently only "sftp" is + // supported. The field is optional since CloudWalker has been retired. + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Enum=sftp + Variant FileVariant `json:"variant,omitempty"` + + // Visibility defines who can see and subscribe to this file type + // +kubebuilder:validation:Enum=World;Zone;Enterprise + // +kubebuilder:default=Enterprise + Visibility Visibility `json:"visibility"` + + // PublicKeys are the SSH public keys registered for the producer's SFTP user. + // At least one key is required. Both label and key value must be unique per fileType. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + PublicKeys []PublicKey `json:"publicKeys"` +} + +// FileSubscription defines a file type that this Rover consumes via SFTP. +// Applying it registers the consumer's SSH public keys on the corresponding +// SFTP user (shared space) created from the matching FileSpecification. +type FileSubscription struct { + // FileType identifies the file type to consume. It must match the + // name of an applied FileSpecification. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + FileType string `json:"fileType"` + + // Variant selects the file-transfer backend. Currently only "sftp" is + // supported. The field is optional since CloudWalker has been retired. + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Enum=sftp + Variant FileVariant `json:"variant,omitempty"` + + // PublicKeys are the SSH public keys registered for the consumer's SFTP user. + // At least one key is required. Both label and key value must be unique per fileType. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + PublicKeys []PublicKey `json:"publicKeys"` +} + +// PublicKey is a labelled SSH public key registered on a SFTP user. +type PublicKey struct { + // Label is a human-readable identifier for the key. It must be unique per fileType. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + Label string `json:"label"` + + // Key is the SSH public key value. It must be unique per fileType. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + // Approval defines the approval workflow for API exposure type Approval struct { // Strategy defines the approval process required for this API diff --git a/rover/api/v1/rover_types_test.go b/rover/api/v1/rover_types_test.go index 35b0855e8..cae38bb30 100644 --- a/rover/api/v1/rover_types_test.go +++ b/rover/api/v1/rover_types_test.go @@ -168,7 +168,7 @@ var _ = Describe("Rover V1 Test Suite", func() { Expect(len(statusErr.Status().Details.Causes)).To(Equal(2)) Expect(statusErr.Status().Details.Causes).To(ContainElement(metav1.StatusCause{ Type: metav1.CauseTypeFieldValueInvalid, - Message: "Invalid value: \"object\": Only one of api or event can be specified (XOR relationship)", + Message: "Invalid value: \"object\": Only one of api, event or file can be specified (XOR relationship)", Field: "spec.exposures[0]", })) @@ -390,4 +390,128 @@ var _ = Describe("Rover V1 Test Suite", func() { Expect(len(statusErr.Status().Details.Causes)).To(Equal(2)) }) }) + + Context("File Types (SFTP)", func() { + It("should report the exposure and subscription type as file", func() { + exp := v1.Exposure{File: &v1.FileExposure{FileType: "demo-sftp-spec-v1"}} + Expect(exp.Type()).To(Equal(v1.TypeFile)) + + sub := v1.Subscription{File: &v1.FileSubscription{FileType: "demo-sftp-spec-v1"}} + Expect(sub.Type()).To(Equal(v1.TypeFile)) + }) + + It("should only support file types on the cetus and canis zones", func() { + Expect(v1.IsFileTypeZoneSupported("cetus")).To(BeTrue()) + Expect(v1.IsFileTypeZoneSupported("canis")).To(BeTrue()) + Expect(v1.IsFileTypeZoneSupported("aws")).To(BeFalse()) + Expect(v1.IsFileTypeZoneSupported("gaia")).To(BeFalse()) + Expect(v1.IsFileTypeZoneSupported("")).To(BeFalse()) + }) + + It("should accept a Rover with a file type exposure and subscription", func() { + rover := new(v1.Rover) + rover.Name = "file-rover" + rover.Namespace = "default" + rover.Spec = v1.RoverSpec{ + Zone: "cetus", + ClientSecret: "topsecret", + Exposures: []v1.Exposure{ + { + File: &v1.FileExposure{ + FileType: "demo-sftp-spec-v1", + Variant: v1.FileVariantSFTP, + Visibility: v1.VisibilityWorld, + PublicKeys: []v1.PublicKey{ + {Label: "demo-provider-key", Key: "ssh-ed25519 AAAAprovider"}, + }, + }, + }, + }, + Subscriptions: []v1.Subscription{ + { + File: &v1.FileSubscription{ + FileType: "demo-sftp-spec-v1", + PublicKeys: []v1.PublicKey{ + {Label: "demo-consumer-key", Key: "ssh-ed25519 AAAAconsumer"}, + }, + }, + }, + }, + } + rover.Status = v1.RoverStatus{} + + err := k8sClient.Create(ctx, rover) + Expect(err).NotTo(HaveOccurred()) + + err = k8sClient.Delete(ctx, rover) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should reject a file type exposure without any public keys", func() { + rover := new(v1.Rover) + rover.Name = "invalid-file-rover" + rover.Namespace = "default" + rover.Spec = v1.RoverSpec{ + Zone: "cetus", + ClientSecret: "topsecret", + Exposures: []v1.Exposure{ + { + File: &v1.FileExposure{ + FileType: "demo-sftp-spec-v1", + Visibility: v1.VisibilityWorld, + PublicKeys: []v1.PublicKey{}, + }, + }, + }, + } + rover.Status = v1.RoverStatus{} + + err := k8sClient.Create(ctx, rover) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsInvalid(err)).To(BeTrue()) + }) + + It("should reject a file type exposure combined with an api exposure in the same entry", func() { + rover := new(v1.Rover) + rover.Name = "invalid-file-rover" + rover.Namespace = "default" + rover.Spec = v1.RoverSpec{ + Zone: "cetus", + ClientSecret: "topsecret", + Exposures: []v1.Exposure{ + { + Api: &v1.ApiExposure{ + BasePath: "/api", + Upstreams: []v1.Upstream{ + {URL: "http://example.com"}, + }, + Visibility: v1.VisibilityEnterprise, + Approval: v1.Approval{ + Strategy: v1.ApprovalStrategyAuto, + }, + }, + File: &v1.FileExposure{ + FileType: "demo-sftp-spec-v1", + Visibility: v1.VisibilityWorld, + PublicKeys: []v1.PublicKey{ + {Label: "demo-provider-key", Key: "ssh-ed25519 AAAAprovider"}, + }, + }, + }, + }, + } + rover.Status = v1.RoverStatus{} + + err := k8sClient.Create(ctx, rover) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsInvalid(err)).To(BeTrue()) + statusErr, ok := err.(apierrors.APIStatus) + Expect(ok).To(BeTrue()) + Expect(statusErr.Status().Details.Causes).To(ContainElement(metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueInvalid, + Message: "Invalid value: \"object\": Only one of api, event or file can be specified (XOR relationship)", + Field: "spec.exposures[0]", + })) + }) + }) }) diff --git a/rover/api/v1/zz_generated.deepcopy.go b/rover/api/v1/zz_generated.deepcopy.go index 24adcd052..b587de318 100644 --- a/rover/api/v1/zz_generated.deepcopy.go +++ b/rover/api/v1/zz_generated.deepcopy.go @@ -680,6 +680,11 @@ func (in *Exposure) DeepCopyInto(out *Exposure) { *out = new(EventExposure) (*in).DeepCopyInto(*out) } + if in.File != nil { + in, out := &in.File, &out.File + *out = new(FileExposure) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Exposure. @@ -732,6 +737,143 @@ func (in *ExternalIdentityProvider) DeepCopy() *ExternalIdentityProvider { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileExposure) DeepCopyInto(out *FileExposure) { + *out = *in + if in.PublicKeys != nil { + in, out := &in.PublicKeys, &out.PublicKeys + *out = make([]PublicKey, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileExposure. +func (in *FileExposure) DeepCopy() *FileExposure { + if in == nil { + return nil + } + out := new(FileExposure) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileSpecification) DeepCopyInto(out *FileSpecification) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSpecification. +func (in *FileSpecification) DeepCopy() *FileSpecification { + if in == nil { + return nil + } + out := new(FileSpecification) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FileSpecification) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileSpecificationList) DeepCopyInto(out *FileSpecificationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]FileSpecification, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSpecificationList. +func (in *FileSpecificationList) DeepCopy() *FileSpecificationList { + if in == nil { + return nil + } + out := new(FileSpecificationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FileSpecificationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileSpecificationSpec) DeepCopyInto(out *FileSpecificationSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSpecificationSpec. +func (in *FileSpecificationSpec) DeepCopy() *FileSpecificationSpec { + if in == nil { + return nil + } + out := new(FileSpecificationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileSpecificationStatus) DeepCopyInto(out *FileSpecificationStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.FileType.DeepCopyInto(&out.FileType) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSpecificationStatus. +func (in *FileSpecificationStatus) DeepCopy() *FileSpecificationStatus { + if in == nil { + return nil + } + out := new(FileSpecificationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileSubscription) DeepCopyInto(out *FileSubscription) { + *out = *in + if in.PublicKeys != nil { + in, out := &in.PublicKeys, &out.PublicKeys + *out = make([]PublicKey, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSubscription. +func (in *FileSubscription) DeepCopy() *FileSubscription { + if in == nil { + return nil + } + out := new(FileSubscription) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Failover) DeepCopyInto(out *Failover) { *out = *in @@ -1273,6 +1415,21 @@ func (in *RoverSpec) DeepCopy() *RoverSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PublicKey) DeepCopyInto(out *PublicKey) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicKey. +func (in *PublicKey) DeepCopy() *PublicKey { + if in == nil { + return nil + } + out := new(PublicKey) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RoverStatus) DeepCopyInto(out *RoverStatus) { *out = *in @@ -1315,6 +1472,20 @@ func (in *RoverStatus) DeepCopyInto(out *RoverStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.FileExposures != nil { + in, out := &in.FileExposures, &out.FileExposures + *out = make([]types.ObjectRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.FileSubscriptions != nil { + in, out := &in.FileSubscriptions, &out.FileSubscriptions + *out = make([]types.ObjectRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.PermissionSets != nil { in, out := &in.PermissionSets, &out.PermissionSets *out = make([]types.ObjectRef, len(*in)) @@ -1437,6 +1608,11 @@ func (in *Subscription) DeepCopyInto(out *Subscription) { *out = new(EventSubscription) (*in).DeepCopyInto(*out) } + if in.File != nil { + in, out := &in.File, &out.File + *out = new(FileSubscription) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Subscription. diff --git a/rover/cmd/main.go b/rover/cmd/main.go index 7a0a50b17..954ebb66a 100644 --- a/rover/cmd/main.go +++ b/rover/cmd/main.go @@ -9,6 +9,7 @@ import ( "flag" "os" + filev1 "github.com/telekom/controlplane/file/api/v1" "sigs.k8s.io/controller-runtime/pkg/metrics" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) @@ -64,6 +65,9 @@ func init() { if cconfig.FeaturePermission.IsEnabled() { utilruntime.Must(permissionv1.AddToScheme(scheme)) } + if cconfig.FeatureFile.IsEnabled() { + utilruntime.Must(filev1.AddToScheme(scheme)) + } //+kubebuilder:scaffold:scheme } @@ -209,6 +213,16 @@ func main() { } } + if cconfig.FeatureFile.IsEnabled() { + if err = (&controller.FileSpecificationReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "FileSpecification") + os.Exit(1) + } + } + if err = (&controller.ApiChangelogReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), diff --git a/rover/config/crd/bases/rover.cp.ei.telekom.de_filespecifications.yaml b/rover/config/crd/bases/rover.cp.ei.telekom.de_filespecifications.yaml new file mode 100644 index 000000000..e43189330 --- /dev/null +++ b/rover/config/crd/bases/rover.cp.ei.telekom.de_filespecifications.yaml @@ -0,0 +1,152 @@ +# SPDX-FileCopyrightText: 2026 Deutsche Telekom IT GmbH +# +# SPDX-License-Identifier: Apache-2.0 +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.1 + name: filespecifications.rover.cp.ei.telekom.de +spec: + group: rover.cp.ei.telekom.de + names: + kind: FileSpecification + listKind: FileSpecificationList + plural: filespecifications + singular: filespecification + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: |- + FileSpecification is the Schema for the filespecifications API. + It defines a file type's metadata and creates the corresponding file-domain + FileType, analogous to how ApiSpecification creates Api resources and + EventSpecification creates EventType resources. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: FileSpecificationSpec defines the desired state of FileSpecification. + properties: + description: + description: Description provides a human-readable summary of this + file type. + type: string + specification: + description: |- + Specification contains the file ID reference from the file manager for the + optional document that describes this file type. + type: string + storageType: + default: sftp + description: 'StorageType selects the file-transfer backend. Currently + only "sftp".' + enum: + - sftp + type: string + type: object + status: + description: FileSpecificationStatus defines the observed state of FileSpecification. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + fileType: + description: FileType references the file-domain resource created from + this specification. + properties: + name: + type: string + namespace: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + - namespace + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} + diff --git a/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml b/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml index eaaf57448..88eafac46 100644 --- a/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml +++ b/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml @@ -608,13 +608,68 @@ spec: - eventType - visibility type: object + file: + description: File defines a File-based (SFTP) service exposure + configuration + properties: + fileType: + description: |- + FileType identifies the file type that is exposed. It must match the + name (and spec.type) of an applied FileSpecification. + minLength: 1 + type: string + publicKeys: + description: |- + PublicKeys are the SSH public keys registered for the producer's SFTP user. + At least one key is required. Both label and key value must be unique per fileType. + items: + description: PublicKey is a labelled SSH public key registered + on a SFTP user. + properties: + key: + description: Key is the SSH public key value. It must + be unique per fileType. + minLength: 1 + type: string + label: + description: Label is a human-readable identifier for + the key. It must be unique per fileType. + minLength: 1 + type: string + required: + - key + - label + type: object + minItems: 1 + type: array + variant: + description: |- + Variant selects the file-transfer backend. Currently only "sftp" is + supported. + enum: + - sftp + type: string + visibility: + default: Enterprise + description: Visibility defines who can see and subscribe + to this file type + enum: + - World + - Zone + - Enterprise + type: string + required: + - fileType + - publicKeys + - visibility + type: object type: object x-kubernetes-validations: - - message: At least one of api or event must be specified - rule: self == null || has(self.api) || has(self.event) - - message: Only one of api or event can be specified (XOR relationship) - rule: self == null || (!has(self.api) && has(self.event)) || (has(self.api) - && !has(self.event)) + - message: At least one of api, event or file must be specified + rule: self == null || has(self.api) || has(self.event) || has(self.file) + - message: Only one of api, event or file can be specified (XOR relationship) + rule: self == null || [has(self.api), has(self.event), has(self.file)].filter(x, + x).size() == 1 type: array externalIds: description: |- @@ -1002,13 +1057,58 @@ spec: - delivery - eventType type: object + file: + description: File defines a File-based (SFTP) service subscription + configuration + properties: + fileType: + description: |- + FileType identifies the file type to consume. It must match the + name (and spec.type) of an applied FileSpecification. + minLength: 1 + type: string + publicKeys: + description: |- + PublicKeys are the SSH public keys registered for the consumer's SFTP user. + At least one key is required. Both label and key value must be unique per fileType. + items: + description: PublicKey is a labelled SSH public key registered + on a SFTP user. + properties: + key: + description: Key is the SSH public key value. It must + be unique per fileType. + minLength: 1 + type: string + label: + description: Label is a human-readable identifier for + the key. It must be unique per fileType. + minLength: 1 + type: string + required: + - key + - label + type: object + minItems: 1 + type: array + variant: + description: |- + Variant selects the file-transfer backend. Currently only "sftp" is + supported. + enum: + - sftp + type: string + required: + - fileType + - publicKeys + type: object type: object x-kubernetes-validations: - - message: At least one of api or event must be specified - rule: self == null || has(self.api) || has(self.event) - - message: Only one of api or event can be specified (XOR relationship) - rule: (has(self.api) && !has(self.event)) || (!has(self.api) && - has(self.event)) + - message: At least one of api, event or file must be specified + rule: self == null || has(self.api) || has(self.event) || has(self.file) + - message: Only one of api, event or file can be specified (XOR relationship) + rule: self == null || [has(self.api), has(self.event), has(self.file)].filter(x, + x).size() == 1 type: array zone: description: Zone identifies the deployment zone for this Rover resource @@ -1191,6 +1291,52 @@ spec: - namespace type: object type: array + fileExposures: + description: FileExposures are references to FileExposure resources + created by this Rover in the file domain + items: + description: |- + ObjectRef is a reference to a Kubernetes object + It is similar to types.NamespacedName but has the required json tags for serialization + properties: + name: + type: string + namespace: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + - namespace + type: object + type: array + fileSubscriptions: + description: FileSubscriptions are references to FileSubscription resources + created by this Rover in the file domain + items: + description: |- + ObjectRef is a reference to a Kubernetes object + It is similar to types.NamespacedName but has the required json tags for serialization + properties: + name: + type: string + namespace: + type: string + uid: + description: |- + UID is a type that holds unique ID values, including UUIDs. Because we + don't ONLY use UUIDs, this is an alias to string. Being a type captures + intent and helps make sure that UIDs and names do not get conflated. + type: string + required: + - name + - namespace + type: object + type: array permissionSets: description: PermissionSets are references to PermissionSet resources created by this Rover diff --git a/rover/config/crd/kustomization.yaml b/rover/config/crd/kustomization.yaml index bd56bb058..2d764acf4 100644 --- a/rover/config/crd/kustomization.yaml +++ b/rover/config/crd/kustomization.yaml @@ -9,6 +9,7 @@ resources: - bases/rover.cp.ei.telekom.de_rovers.yaml - bases/rover.cp.ei.telekom.de_apispecifications.yaml - bases/rover.cp.ei.telekom.de_eventspecifications.yaml +- bases/rover.cp.ei.telekom.de_filespecifications.yaml - bases/rover.cp.ei.telekom.de_apichangelogs.yaml - bases/rover.cp.ei.telekom.de_roadmaps.yaml #+kubebuilder:scaffold:crdkustomizeresource diff --git a/rover/config/rbac/role.yaml b/rover/config/rbac/role.yaml index 94f310dd4..6277af762 100644 --- a/rover/config/rbac/role.yaml +++ b/rover/config/rbac/role.yaml @@ -78,6 +78,20 @@ rules: - patch - update - watch +- apiGroups: + - file.ei.telekom.de + resources: + - fileexposures + - filesubscriptions + - filetypes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - organization.cp.ei.telekom.de resources: @@ -104,6 +118,7 @@ rules: - apichangelogs - apispecifications - eventspecifications + - filespecifications - roadmaps - rovers verbs: @@ -120,6 +135,7 @@ rules: - apichangelogs/finalizers - apispecifications/finalizers - eventspecifications/finalizers + - filespecifications/finalizers - roadmaps/finalizers - rovers/finalizers verbs: @@ -130,6 +146,7 @@ rules: - apichangelogs/status - apispecifications/status - eventspecifications/status + - filespecifications/status - roadmaps/status - rovers/status verbs: diff --git a/rover/config/webhook/manifests.yaml b/rover/config/webhook/manifests.yaml index bd7bbccd5..af7bf915b 100644 --- a/rover/config/webhook/manifests.yaml +++ b/rover/config/webhook/manifests.yaml @@ -73,6 +73,26 @@ webhooks: resources: - apispecifications sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-rover-cp-ei-telekom-de-v1-filespecification + failurePolicy: Fail + name: vfilespecification-v1.kb.io + rules: + - apiGroups: + - rover.cp.ei.telekom.de + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - filespecifications + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/rover/go.mod b/rover/go.mod index 5fbbe5ab8..693036da7 100644 --- a/rover/go.mod +++ b/rover/go.mod @@ -25,6 +25,7 @@ require ( github.com/onsi/gomega v1.40.0 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.11.1 + github.com/telekom/controlplane/file/api v0.0.0-00010101000000-000000000000 k8s.io/api v0.36.0 k8s.io/apimachinery v0.36.0 k8s.io/client-go v0.36.0 @@ -38,6 +39,7 @@ replace ( github.com/telekom/controlplane/common => ../common github.com/telekom/controlplane/common-server => ../common-server github.com/telekom/controlplane/event/api => ../event/api + github.com/telekom/controlplane/file/api => ../file/api github.com/telekom/controlplane/organization/api => ../organization/api github.com/telekom/controlplane/permission/api => ../permission/api github.com/telekom/controlplane/rover/api => ./api diff --git a/rover/internal/controller/filespecification_controller.go b/rover/internal/controller/filespecification_controller.go new file mode 100644 index 000000000..0cc9052cc --- /dev/null +++ b/rover/internal/controller/filespecification_controller.go @@ -0,0 +1,57 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package controller + +import ( + "context" + + cconfig "github.com/telekom/controlplane/common/pkg/config" + cc "github.com/telekom/controlplane/common/pkg/controller" + filev1 "github.com/telekom/controlplane/file/api/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + + filespec_handler "github.com/telekom/controlplane/rover/internal/handler/filespecification" + + rover "github.com/telekom/controlplane/rover/api/v1" +) + +// FileSpecificationReconciler reconciles a FileSpecification object +type FileSpecificationReconciler struct { + client.Client + Scheme *runtime.Scheme + Recorder record.EventRecorder + + cc.Controller[*rover.FileSpecification] +} + +// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch + +// +kubebuilder:rbac:groups=rover.cp.ei.telekom.de,resources=filespecifications,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=rover.cp.ei.telekom.de,resources=filespecifications/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=rover.cp.ei.telekom.de,resources=filespecifications/finalizers,verbs=update +// +kubebuilder:rbac:groups=file.ei.telekom.de,resources=filetypes,verbs=get;list;watch;create;update;patch;delete + +func (r *FileSpecificationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + return r.Controller.Reconcile(ctx, req, &rover.FileSpecification{}) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *FileSpecificationReconciler) SetupWithManager(mgr ctrl.Manager) error { + r.Recorder = mgr.GetEventRecorderFor("filespecification-controller") + r.Controller = cc.NewController(&filespec_handler.FileSpecificationHandler{}, r.Client, r.Recorder) + + return ctrl.NewControllerManagedBy(mgr). + For(&rover.FileSpecification{}). + Owns(&filev1.FileType{}). + WithOptions(controller.Options{ + MaxConcurrentReconciles: cconfig.MaxConcurrentReconciles, + RateLimiter: cc.NewRateLimiter(), + }). + Complete(r) +} diff --git a/rover/internal/controller/rover_controller.go b/rover/internal/controller/rover_controller.go index 2281b5ebe..374c37837 100644 --- a/rover/internal/controller/rover_controller.go +++ b/rover/internal/controller/rover_controller.go @@ -25,6 +25,7 @@ import ( apiapi "github.com/telekom/controlplane/api/api/v1" application "github.com/telekom/controlplane/application/api/v1" eventv1 "github.com/telekom/controlplane/event/api/v1" + filev1 "github.com/telekom/controlplane/file/api/v1" organizationv1 "github.com/telekom/controlplane/organization/api/v1" permissionv1 "github.com/telekom/controlplane/permission/api/v1" rover "github.com/telekom/controlplane/rover/api/v1" @@ -82,6 +83,11 @@ func (r *RoverReconciler) SetupWithManager(mgr ctrl.Manager) error { b = b.Owns(&permissionv1.PermissionSet{}) } + if cconfig.FeatureFile.IsEnabled() { + b = b.Owns(&filev1.FileExposure{}). + Owns(&filev1.FileSubscription{}) + } + b = b.Watches(&organizationv1.Team{}, handler.EnqueueRequestsFromMapFunc(r.MapTeamToRovers), builder.WithPredicates(predicate.GenerationChangedPredicate{}), diff --git a/rover/internal/handler/filespecification/handler.go b/rover/internal/handler/filespecification/handler.go new file mode 100644 index 000000000..c98314830 --- /dev/null +++ b/rover/internal/handler/filespecification/handler.go @@ -0,0 +1,78 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package filespecification + +import ( + "context" + + "github.com/pkg/errors" + "github.com/telekom/controlplane/common/pkg/client" + "github.com/telekom/controlplane/common/pkg/condition" + "github.com/telekom/controlplane/common/pkg/handler" + "github.com/telekom/controlplane/common/pkg/types" + "github.com/telekom/controlplane/common/pkg/util/labelutil" + filev1 "github.com/telekom/controlplane/file/api/v1" + roverv1 "github.com/telekom/controlplane/rover/api/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +var _ handler.Handler[*roverv1.FileSpecification] = (*FileSpecificationHandler)(nil) + +// FileSpecificationHandler reconciles a rover-domain FileSpecification into a +// file-domain FileType (mirrors EventSpecificationHandler -> EventType). +type FileSpecificationHandler struct{} + +func (h *FileSpecificationHandler) CreateOrUpdate(ctx context.Context, fileSpec *roverv1.FileSpecification) error { + c := client.ClientFromContextOrDie(ctx) + + // The FileType name is derived from the FileSpecification name (the file type + // identifier); they live in the same namespace. + name := roverv1.MakeFileSpecificationName(fileSpec) + + fileType := &filev1.FileType{ + ObjectMeta: metav1.ObjectMeta{ + Name: labelutil.NormalizeNameValue(name), + Namespace: fileSpec.Namespace, + }, + } + + fileSpec.Status.FileType = *types.ObjectRefFromObject(fileType) + + mutator := func() error { + if err := controllerutil.SetControllerReference(fileSpec, fileType, c.Scheme()); err != nil { + return errors.Wrap(err, "failed to set controller reference") + } + + fileType.Labels = map[string]string{ + filev1.FileTypeLabelKey: labelutil.NormalizeLabelValue(fileSpec.Name), + } + + fileType.Spec = filev1.FileTypeSpec{ + Type: fileSpec.Name, + Description: fileSpec.Spec.Description, + Specification: fileSpec.Spec.Specification, + } + return nil + } + + if _, err := c.CreateOrUpdate(ctx, fileType, mutator); err != nil { + return errors.Wrap(err, "failed to create or update FileType") + } + + if c.AnyChanged() { + fileSpec.SetCondition(condition.NewProcessingCondition("Provisioning", "FileType updated")) + fileSpec.SetCondition(condition.NewNotReadyCondition("Provisioning", "FileType is not ready")) + } else { + fileSpec.SetCondition(condition.NewDoneProcessingCondition("FileType created")) + fileSpec.SetCondition(condition.NewReadyCondition("Provisioned", "FileType is ready")) + } + + return nil +} + +func (h *FileSpecificationHandler) Delete(ctx context.Context, obj *roverv1.FileSpecification) error { + return nil +} diff --git a/rover/internal/handler/rover/application/application.go b/rover/internal/handler/rover/application/application.go index 0e0d06d76..4dcef5e73 100644 --- a/rover/internal/handler/rover/application/application.go +++ b/rover/internal/handler/rover/application/application.go @@ -47,14 +47,9 @@ func HandleApplication(ctx context.Context, c client.JanitorClient, owner *rover } else if err != nil { return err } - // If the Application publishes any events, we need to create a client for it, even if it doesn't have any subscriptions. // This is because the client is needed to access the publish-route - hasAnyEventExposures := slices.ContainsFunc(owner.Spec.Exposures, func(ex roverv1.Exposure) bool { - return ex.Type() == roverv1.TypeEvent - }) - - needsClient := len(owner.Spec.Subscriptions) > 0 || hasAnyEventExposures + needsClient := RoverNeedsClient(owner) var subscriberFailoverZones []types.ObjectRef if needsClient { for _, subscription := range owner.Spec.Subscriptions { @@ -136,3 +131,19 @@ func HandleApplication(ctx context.Context, c client.JanitorClient, owner *rover return err } + +// RoverNeedsClient reports whether the Application derived from the given Rover +// requires an Identity client (and Gateway consumer). +// +// File-type (SFTP) subscriptions and exposures are realized in the file domain via +// SFTP users and SSH public keys. They neither need an Identity client nor a Gateway +// consumer. Only non-file subscriptions or any event exposure force a client/consumer. +func RoverNeedsClient(owner *roverv1.Rover) bool { + hasAnyEventExposures := slices.ContainsFunc(owner.Spec.Exposures, func(ex roverv1.Exposure) bool { + return ex.Type() == roverv1.TypeEvent + }) + hasNonFileSubscriptions := slices.ContainsFunc(owner.Spec.Subscriptions, func(sub roverv1.Subscription) bool { + return sub.Type() != roverv1.TypeFile + }) + return hasNonFileSubscriptions || hasAnyEventExposures +} diff --git a/rover/internal/handler/rover/file/exposure.go b/rover/internal/handler/rover/file/exposure.go index fbe94399d..932e99899 100644 --- a/rover/internal/handler/rover/file/exposure.go +++ b/rover/internal/handler/rover/file/exposure.go @@ -11,6 +11,7 @@ import ( "github.com/telekom/controlplane/common/pkg/client" "github.com/telekom/controlplane/common/pkg/config" "github.com/telekom/controlplane/common/pkg/types" + "github.com/telekom/controlplane/common/pkg/util/contextutil" "github.com/telekom/controlplane/common/pkg/util/labelutil" filev1 "github.com/telekom/controlplane/file/api/v1" roverv1 "github.com/telekom/controlplane/rover/api/v1" @@ -33,11 +34,10 @@ func HandleExposure(ctx context.Context, c client.JanitorClient, owner *roverv1. }, } - // The FileType is created from the FileSpecification in the same namespace - // as the Rover exposure, with its name derived from the file type identifier. - fileTypeRef := types.ObjectRef{ - Name: filev1.MakeFileTypeName(exp.FileType), - Namespace: owner.Namespace, + environment := contextutil.EnvFromContextOrDie(ctx) + zoneRef := types.ObjectRef{ + Name: owner.Spec.Zone, + Namespace: environment, } mutator := func() error { @@ -47,15 +47,16 @@ func HandleExposure(ctx context.Context, c client.JanitorClient, owner *roverv1. fileExposure.Labels = map[string]string{ filev1.FileTypeLabelKey: labelutil.NormalizeLabelValue(exp.FileType), - config.BuildLabelKey("zone"): labelutil.NormalizeLabelValue(owner.Spec.Zone), + config.BuildLabelKey("zone"): labelutil.NormalizeLabelValue(zoneRef.Name), config.BuildLabelKey("application"): labelutil.NormalizeLabelValue(owner.Name), } fileExposure.Spec = filev1.FileExposureSpec{ - Approval: filev1.ApprovalStrategySimple, - Visibility: filev1.Visibility(exp.Visibility.String()), - FileTypeRef: fileTypeRef, - PublicKeys: mapPublicKeys(exp.PublicKeys), + Approval: filev1.Approval{Strategy: filev1.ApprovalStrategySimple}, + Visibility: filev1.Visibility(exp.Visibility.String()), + FileType: exp.FileType, + PublicKeys: mapPublicKeys(exp.PublicKeys), + Zone: zoneRef, } return nil } diff --git a/rover/internal/handler/rover/file/subscription.go b/rover/internal/handler/rover/file/subscription.go index 825b137d5..de9bf5387 100644 --- a/rover/internal/handler/rover/file/subscription.go +++ b/rover/internal/handler/rover/file/subscription.go @@ -33,11 +33,6 @@ func HandleSubscription(ctx context.Context, c client.JanitorClient, owner *rove }, } - fileTypeRef := types.ObjectRef{ - Name: filev1.MakeFileTypeName(sub.FileType), - Namespace: owner.Namespace, - } - mutator := func() error { if err := controllerutil.SetControllerReference(owner, fileSubscription, c.Scheme()); err != nil { return errors.Wrap(err, "failed to set controller reference") @@ -50,8 +45,8 @@ func HandleSubscription(ctx context.Context, c client.JanitorClient, owner *rove } fileSubscription.Spec = filev1.FileSubscriptionSpec{ - FileTypeRef: fileTypeRef, - PublicKeys: mapPublicKeys(sub.PublicKeys), + FileType: sub.FileType, + PublicKeys: mapPublicKeys(sub.PublicKeys), } return nil } diff --git a/rover/internal/webhook/v1/filespecification_webhook.go b/rover/internal/webhook/v1/filespecification_webhook.go index 0a743f102..5fde27141 100644 --- a/rover/internal/webhook/v1/filespecification_webhook.go +++ b/rover/internal/webhook/v1/filespecification_webhook.go @@ -56,12 +56,15 @@ func (v *FileSpecificationCustomValidator) ValidateCreateOrUpdate(ctx context.Co valErr := cerrors.NewValidationError(roverv1.GroupVersion.WithKind("FileSpecification").GroupKind(), filespecification) - // metadata.name must be equal to spec.type - if filespecification.GetName() != filespecification.Spec.Type { + // storageType, when set, must be a supported backend (currently only "sftp"). + // The file type identifier lives in metadata.name (no spec.type field in the + // internal CRD, per spec_dcp); the client-side name==type rule is enforced by + // rover-server / roverctl. + if st := filespecification.Spec.StorageType; st != "" && st != roverv1.FileStorageTypeSFTP { valErr.AddInvalidError( - field.NewPath("spec").Child("type"), - filespecification.Spec.Type, - fmt.Sprintf("spec.type must be equal to metadata.name %q", filespecification.GetName()), + field.NewPath("spec").Child("storageType"), + string(st), + fmt.Sprintf("spec.storageType must be %q", roverv1.FileStorageTypeSFTP), ) } diff --git a/rover/internal/webhook/v1/filespecification_webhook_test.go b/rover/internal/webhook/v1/filespecification_webhook_test.go index cddb2064c..f11fa547f 100644 --- a/rover/internal/webhook/v1/filespecification_webhook_test.go +++ b/rover/internal/webhook/v1/filespecification_webhook_test.go @@ -92,6 +92,49 @@ var _ = Describe("File Type (SFTP) Validation", func() { }) }) + Context("file type zone restriction (Rover webhook)", func() { + var validator RoverValidator + + BeforeEach(func() { + validator = RoverValidator{client: k8sClient} + }) + + fileExposure := func() roverv1.Exposure { + return roverv1.Exposure{File: &roverv1.FileExposure{ + FileType: "demo-sftp-spec-v1", + PublicKeys: []roverv1.PublicKey{{Label: "provider-key", Key: "ssh-ed25519 AAAA"}}, + }} + } + + It("should reject a file exposure on an unsupported zone", func() { + // testZone is named "test" and is not in {cetus, canis}. + rover := NewRover(testZone) + rover.Spec.Exposures = []roverv1.Exposure{fileExposure()} + warnings, err := validator.ValidateCreate(ctx, rover) + assertValidationFailedWith(warnings, err, "does not support file types") + }) + + It("should reject a file subscription on an unsupported zone", func() { + rover := NewRover(testZone) + rover.Spec.Subscriptions = []roverv1.Subscription{{File: &roverv1.FileSubscription{ + FileType: "demo-sftp-spec-v1", + PublicKeys: []roverv1.PublicKey{{Label: "consumer-key", Key: "ssh-ed25519 BBBB"}}, + }}} + warnings, err := validator.ValidateCreate(ctx, rover) + assertValidationFailedWith(warnings, err, "does not support file types") + }) + + It("should accept a file exposure on a supported zone (cetus)", func() { + cetus := NewZone("cetus", testZone.Namespace) + CreateZone(ctx, cetus) + rover := NewRover(cetus) + rover.Spec.Exposures = []roverv1.Exposure{fileExposure()} + warnings, err := validator.ValidateCreate(ctx, rover) + Expect(warnings).To(BeNil()) + Expect(err).NotTo(HaveOccurred()) + }) + }) + Context("FileSpecificationCustomValidator", func() { var validator *FileSpecificationCustomValidator @@ -99,23 +142,29 @@ var _ = Describe("File Type (SFTP) Validation", func() { validator = &FileSpecificationCustomValidator{client: k8sClient} }) - newFileSpec := func(name, specType string) *roverv1.FileSpecification { + newFileSpec := func(name string, storageType roverv1.FileStorageType) *roverv1.FileSpecification { return &roverv1.FileSpecification{ ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "default"}, - Spec: roverv1.FileSpecificationSpec{Type: specType, Version: "1.0.0"}, + Spec: roverv1.FileSpecificationSpec{Description: "demo", StorageType: storageType}, } } - It("should accept a FileSpecification whose name equals spec.type", func() { - warnings, err := validator.ValidateCreate(ctx, newFileSpec("demo-sftp-spec-v1", "demo-sftp-spec-v1")) + It("should accept a FileSpecification with the sftp storageType", func() { + warnings, err := validator.ValidateCreate(ctx, newFileSpec("demo-sftp-spec-v1", roverv1.FileStorageTypeSFTP)) + Expect(warnings).To(BeNil()) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should accept a FileSpecification with an empty storageType (defaulted by CRD)", func() { + warnings, err := validator.ValidateCreate(ctx, newFileSpec("demo-sftp-spec-v1", "")) Expect(warnings).To(BeNil()) Expect(err).NotTo(HaveOccurred()) }) - It("should reject a FileSpecification whose name differs from spec.type", func() { - _, err := validator.ValidateCreate(ctx, newFileSpec("demo-sftp-spec-v1", "other-type")) + It("should reject a FileSpecification with an unsupported storageType", func() { + _, err := validator.ValidateCreate(ctx, newFileSpec("demo-sftp-spec-v1", "s3")) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("spec.type must be equal to metadata.name")) + Expect(err.Error()).To(ContainSubstring("spec.storageType must be")) }) }) }) From a8ce3f6fd71a64ad7400bc5b257f6962a04f69cf Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Wed, 8 Jul 2026 14:50:31 +0300 Subject: [PATCH 10/31] feat(dhei-20905): add tests and info files --- docs/docs/architecture/rover.mdx | 16 ++ .../internal/mapper/rover/in/file_test.go | 156 ++++++++++++++++++ rover/PROJECT | 12 ++ .../rover/application/application_test.go | 55 ++++++ 4 files changed, 239 insertions(+) create mode 100644 rover-server/internal/mapper/rover/in/file_test.go create mode 100644 rover/internal/handler/rover/application/application_test.go diff --git a/docs/docs/architecture/rover.mdx b/docs/docs/architecture/rover.mdx index 9cdcb38c7..aab77db7e 100644 --- a/docs/docs/architecture/rover.mdx +++ b/docs/docs/architecture/rover.mdx @@ -25,10 +25,26 @@ Users interact with the Rover domain through three paths: - **Application domain** — Creates Application resources. - **API domain** — Creates Api, ApiExposure, and ApiSubscription resources. - **Event domain** — Creates EventExposure and EventSubscription resources. +- **File domain (SFTP)** — Creates the SFTP user (shared space) from a `FileSpecification` and registers producer/consumer SSH public keys for `fileType` exposures and subscriptions. Gated behind the `file` feature flag. - **Gateway domain** — Configures traffic management settings. - **Identity domain** — Configures authentication settings. - **Approval domain** — Integrates approval requirements for exposures. +## File Types (SFTP) + +Rover can configure an external SFTP file-transfer service without changing the customer-facing Rover file: + +- A `FileSpecification` (`metadata.name` must equal `spec.type`) provisions an SFTP user with a default shared space. +- A `fileType` **exposure** (producer) or **subscription** (consumer) with `variant: sftp` and one or more `publicKeys` registers those SSH keys on the matching SFTP user. + +Validation rules enforced by the Rover webhook: + +- `fileType` exposures/subscriptions are only allowed on the `cetus` and `canis` zones. +- `variant: sftp` is optional. +- At least one public key is required; both the key `label` and `key` value must be unique per `fileType`. + +Because file transfer happens directly over SFTP, a Rover that only exposes/subscribes to file types yields a **logical** Application (`needsClient` and `needsConsumer` are `false`). If the same Rover also has an API or event subscription, a client is still required. + ## Related Pages - [User Journey: Onboarding](../user-journey/onboarding.md) diff --git a/rover-server/internal/mapper/rover/in/file_test.go b/rover-server/internal/mapper/rover/in/file_test.go new file mode 100644 index 000000000..62cec9fdf --- /dev/null +++ b/rover-server/internal/mapper/rover/in/file_test.go @@ -0,0 +1,156 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package in + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + roverv1 "github.com/telekom/controlplane/rover/api/v1" + + "github.com/telekom/controlplane/rover-server/internal/api" +) + +var _ = Describe("File Type (SFTP) Mapper", func() { + + Context("mapFileExposure", func() { + It("must map a FileExposure correctly", func() { + input := api.FileExposure{ + Type: "file", + FileType: "demo-sftp-spec-v1", + Variant: "sftp", + Visibility: api.WORLD, + PublicKeys: []api.PublicKey{ + {Label: "provider-key", Key: "ssh-ed25519 AAAA1"}, + }, + } + + output := mapFileExposure(input) + + Expect(output).ToNot(BeNil()) + Expect(output.FileType).To(Equal("demo-sftp-spec-v1")) + Expect(output.Variant).To(Equal(roverv1.FileVariantSFTP)) + Expect(output.Visibility).To(Equal(roverv1.VisibilityWorld)) + Expect(output.PublicKeys).To(HaveLen(1)) + Expect(output.PublicKeys[0].Label).To(Equal("provider-key")) + Expect(output.PublicKeys[0].Key).To(Equal("ssh-ed25519 AAAA1")) + }) + + It("must leave visibility empty when omitted (CRD default applies at admission)", func() { + // The mapper does not default visibility itself — consistent with + // mapApiExposure/mapEventExposure. An empty value is passed through and + // the CRD's +kubebuilder:default=Enterprise fills it in at admission. + input := api.FileExposure{ + Type: "file", + FileType: "demo-sftp-spec-v1", + PublicKeys: []api.PublicKey{ + {Label: "provider-key", Key: "ssh-ed25519 AAAA1"}, + }, + } + + output := mapFileExposure(input) + + Expect(output.Visibility).To(BeEmpty()) + }) + + It("must leave variant empty when omitted (optional field)", func() { + input := api.FileExposure{ + Type: "file", + FileType: "demo-sftp-spec-v1", + Visibility: api.WORLD, + PublicKeys: []api.PublicKey{ + {Label: "provider-key", Key: "ssh-ed25519 AAAA1"}, + }, + } + + output := mapFileExposure(input) + + Expect(output.Variant).To(BeEmpty()) + }) + }) + + Context("mapFileSubscription", func() { + It("must map a FileSubscription correctly", func() { + input := api.FileSubscription{ + Type: "file", + FileType: "demo-sftp-spec-v1", + Variant: "sftp", + PublicKeys: []api.PublicKey{ + {Label: "consumer-key", Key: "ssh-ed25519 AAAA2"}, + }, + } + + output := mapFileSubscription(input) + + Expect(output).ToNot(BeNil()) + Expect(output.FileType).To(Equal("demo-sftp-spec-v1")) + Expect(output.Variant).To(Equal(roverv1.FileVariantSFTP)) + Expect(output.PublicKeys).To(HaveLen(1)) + Expect(output.PublicKeys[0].Label).To(Equal("consumer-key")) + Expect(output.PublicKeys[0].Key).To(Equal("ssh-ed25519 AAAA2")) + }) + }) + + Context("mapPublicKeys", func() { + It("must return nil for an empty list", func() { + Expect(mapPublicKeys(nil)).To(BeNil()) + Expect(mapPublicKeys([]api.PublicKey{})).To(BeNil()) + }) + + It("must preserve order and values", func() { + output := mapPublicKeys([]api.PublicKey{ + {Label: "a", Key: "k1"}, + {Label: "b", Key: "k2"}, + }) + + Expect(output).To(HaveLen(2)) + Expect(output[0]).To(Equal(roverv1.PublicKey{Label: "a", Key: "k1"})) + Expect(output[1]).To(Equal(roverv1.PublicKey{Label: "b", Key: "k2"})) + }) + }) + + Context("mapExposure dispatch", func() { + It("must map a FileExposure via the discriminator", func() { + exposure := &api.Exposure{} + Expect(exposure.FromFileExposure(api.FileExposure{ + Type: "file", + FileType: "demo-sftp-spec-v1", + PublicKeys: []api.PublicKey{ + {Label: "provider-key", Key: "ssh-ed25519 AAAA1"}, + }, + })).To(Succeed()) + + output := &roverv1.Exposure{} + err := mapExposure(exposure, output) + + Expect(err).To(BeNil()) + Expect(output.File).ToNot(BeNil()) + Expect(output.Api).To(BeNil()) + Expect(output.Event).To(BeNil()) + Expect(output.File.FileType).To(Equal("demo-sftp-spec-v1")) + }) + }) + + Context("mapSubscription dispatch", func() { + It("must map a FileSubscription via the discriminator", func() { + subscription := &api.Subscription{} + Expect(subscription.FromFileSubscription(api.FileSubscription{ + Type: "file", + FileType: "demo-sftp-spec-v1", + PublicKeys: []api.PublicKey{ + {Label: "consumer-key", Key: "ssh-ed25519 AAAA2"}, + }, + })).To(Succeed()) + + output := &roverv1.Subscription{} + err := mapSubscription(subscription, output) + + Expect(err).To(BeNil()) + Expect(output.File).ToNot(BeNil()) + Expect(output.Api).To(BeNil()) + Expect(output.Event).To(BeNil()) + Expect(output.File.FileType).To(Equal("demo-sftp-spec-v1")) + }) + }) +}) diff --git a/rover/PROJECT b/rover/PROJECT index 562e1c9df..1cba46690 100644 --- a/rover/PROJECT +++ b/rover/PROJECT @@ -34,4 +34,16 @@ resources: webhooks: validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: cp.ei.telekom.de + group: rover + kind: FileSpecification + path: github.com/telekom/controlplane/rover/api/v1 + version: v1 + webhooks: + validation: true + webhookVersion: v1 version: "3" diff --git a/rover/internal/handler/rover/application/application_test.go b/rover/internal/handler/rover/application/application_test.go new file mode 100644 index 000000000..7a1b04991 --- /dev/null +++ b/rover/internal/handler/rover/application/application_test.go @@ -0,0 +1,55 @@ +// Copyright 2025 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package application + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + roverv1 "github.com/telekom/controlplane/rover/api/v1" +) + +func TestApplication(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Application Handler Suite") +} + +var _ = Describe("RoverNeedsClient", func() { + newRover := func(exps []roverv1.Exposure, subs []roverv1.Subscription) *roverv1.Rover { + return &roverv1.Rover{ + ObjectMeta: metav1.ObjectMeta{Name: "test-rover", Namespace: "test--eni--hyperion"}, + Spec: roverv1.RoverSpec{Exposures: exps, Subscriptions: subs}, + } + } + + // The concrete field values are irrelevant: Subscription.Type()/Exposure.Type() + // dispatch only on which pointer is non-nil. + apiSub := roverv1.Subscription{Api: &roverv1.ApiSubscription{}} + eventSub := roverv1.Subscription{Event: &roverv1.EventSubscription{}} + fileSub := roverv1.Subscription{File: &roverv1.FileSubscription{}} + eventExp := roverv1.Exposure{Event: &roverv1.EventExposure{}} + fileExp := roverv1.Exposure{File: &roverv1.FileExposure{}} + + DescribeTable("decides whether the derived Application requires an Identity client", + func(exps []roverv1.Exposure, subs []roverv1.Subscription, expected bool) { + Expect(RoverNeedsClient(newRover(exps, subs))).To(Equal(expected)) + }, + // Logical Application (file-only or empty) => no client/consumer. + Entry("empty rover", nil, nil, false), + Entry("file-only subscription", nil, []roverv1.Subscription{fileSub}, false), + Entry("file-only exposure", []roverv1.Exposure{fileExp}, nil, false), + Entry("file exposure + file subscription", []roverv1.Exposure{fileExp}, []roverv1.Subscription{fileSub}, false), + // Non-file subscription or any event exposure => needs client. + Entry("api subscription", nil, []roverv1.Subscription{apiSub}, true), + Entry("event subscription", nil, []roverv1.Subscription{eventSub}, true), + Entry("event exposure", []roverv1.Exposure{eventExp}, nil, true), + // Mixed: file plus a non-file entry still forces a client (story edge case). + Entry("mixed file + api subscription", nil, []roverv1.Subscription{fileSub, apiSub}, true), + Entry("file subscription + event exposure", []roverv1.Exposure{eventExp}, []roverv1.Subscription{fileSub}, true), + ) +}) From b79a651b38bfbe19a8e347e566a6500f6023d04a Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Fri, 17 Jul 2026 17:02:54 +0300 Subject: [PATCH 11/31] feat(dhei-20905): move public keys to the sftp lvl of the file domain and fix the rover mapper out remove zone_supported flag and webhook as it's covered by featureEnabled flag add rbac for file domain --- file/api/v1/fileexposure_types.go | 15 ++++- file/api/v1/filesubscription_types.go | 13 ++++ file/api/v1/types_test.go | 20 +++--- file/api/v1/zz_generated.deepcopy.go | 52 +++++++++++++--- .../file.ei.telekom.de_fileexposures.yaml | 57 +++++++++-------- .../file.ei.telekom.de_filesubscriptions.yaml | 61 +++++++++++-------- .../internal/mapper/rover/out/exposure.go | 28 +++++++++ rover/api/v1/rover_types.go | 12 ---- rover/api/v1/rover_types_test.go | 8 --- rover/internal/controller/rover_controller.go | 3 + rover/internal/handler/rover/file/exposure.go | 6 +- .../handler/rover/file/subscription.go | 6 +- .../v1/filespecification_webhook_test.go | 18 ------ rover/internal/webhook/v1/rover_webhook.go | 14 ----- 14 files changed, 188 insertions(+), 125 deletions(-) diff --git a/file/api/v1/fileexposure_types.go b/file/api/v1/fileexposure_types.go index 764b389f1..230dada4f 100644 --- a/file/api/v1/fileexposure_types.go +++ b/file/api/v1/fileexposure_types.go @@ -25,10 +25,11 @@ type FileExposureSpec struct { // +kubebuilder:validation:MinLength=1 FileType string `json:"fileType"` - // PublicKeys are the SSH public keys registered for the provider's SFTP user. + // Sftp holds the SFTP storage-backend-specific configuration for this exposure. + // Backend-specific settings live under their own sub-object (e.g. sftp) so that + // additional storage backends can be added without polluting the spec root. // +kubebuilder:validation:Required - // +kubebuilder:validation:MinItems=1 - PublicKeys []PublicKey `json:"publicKeys"` + Sftp SftpExposure `json:"sftp"` // Zone references the Zone CR where this file type is exposed. // On this layer only the Zone ref is passed; the file domain resolves it to @@ -36,6 +37,14 @@ type FileExposureSpec struct { Zone ctypes.ObjectRef `json:"zone"` } +// SftpExposure holds the SFTP storage-backend-specific configuration for a FileExposure. +type SftpExposure struct { + // PublicKeys are the SSH public keys registered for the provider's SFTP user. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + PublicKeys []PublicKey `json:"publicKeys"` +} + // FileExposureStatus defines the observed state of FileExposure. type FileExposureStatus struct { // +listType=map diff --git a/file/api/v1/filesubscription_types.go b/file/api/v1/filesubscription_types.go index 21a17d87c..7d2168e8e 100644 --- a/file/api/v1/filesubscription_types.go +++ b/file/api/v1/filesubscription_types.go @@ -19,6 +19,19 @@ type FileSubscriptionSpec struct { // +kubebuilder:validation:MinLength=1 FileType string `json:"fileType"` + // Sftp holds the SFTP storage-backend-specific configuration for this subscription. + // Backend-specific settings live under their own sub-object (e.g. sftp) so that + // additional storage backends can be added without polluting the spec root. + // +kubebuilder:validation:Required + Sftp SftpSubscription `json:"sftp"` +} + +// SftpSubscription holds the SFTP storage-backend-specific configuration for a FileSubscription. +type SftpSubscription struct { + // ClientId identifies the consumer application's client on the SFTP backend. + // +optional + ClientId string `json:"clientId,omitempty"` + // PublicKeys are the SSH public keys registered for the consumer's SFTP user. // +kubebuilder:validation:Required // +kubebuilder:validation:MinItems=1 diff --git a/file/api/v1/types_test.go b/file/api/v1/types_test.go index f78dbc91a..8eef5c01c 100644 --- a/file/api/v1/types_test.go +++ b/file/api/v1/types_test.go @@ -62,8 +62,10 @@ var _ = Describe("FileExposure", func() { Approval: v1.Approval{Strategy: v1.ApprovalStrategySimple}, Visibility: v1.VisibilityEnterprise, FileType: "foo-v1", - PublicKeys: []v1.PublicKey{ - {Label: "provider-key", Key: "ssh-ed25519 AAAA"}, + Sftp: v1.SftpExposure{ + PublicKeys: []v1.PublicKey{ + {Label: "provider-key", Key: "ssh-ed25519 AAAA"}, + }, }, }, Status: v1.FileExposureStatus{ @@ -76,9 +78,9 @@ var _ = Describe("FileExposure", func() { Expect(clone).To(Equal(orig)) // Mutating the clone must not affect the original. - clone.Spec.PublicKeys[0].Key = "changed" + clone.Spec.Sftp.PublicKeys[0].Key = "changed" clone.Status.Subscriptions[0].Name = "other" - Expect(orig.Spec.PublicKeys[0].Key).To(Equal("ssh-ed25519 AAAA")) + Expect(orig.Spec.Sftp.PublicKeys[0].Key).To(Equal("ssh-ed25519 AAAA")) Expect(orig.Status.Subscriptions[0].Name).To(Equal("sub")) }) @@ -93,13 +95,15 @@ var _ = Describe("FileSubscription", func() { It("deep-copies public keys without aliasing", func() { orig := &v1.FileSubscription{ Spec: v1.FileSubscriptionSpec{ - FileType: "foo-v1", - PublicKeys: []v1.PublicKey{{Label: "consumer-key", Key: "ssh-ed25519 BBBB"}}, + FileType: "foo-v1", + Sftp: v1.SftpSubscription{ + PublicKeys: []v1.PublicKey{{Label: "consumer-key", Key: "ssh-ed25519 BBBB"}}, + }, }, } clone := orig.DeepCopy() Expect(clone).To(Equal(orig)) - clone.Spec.PublicKeys[0].Label = "changed" - Expect(orig.Spec.PublicKeys[0].Label).To(Equal("consumer-key")) + clone.Spec.Sftp.PublicKeys[0].Label = "changed" + Expect(orig.Spec.Sftp.PublicKeys[0].Label).To(Equal("consumer-key")) }) }) diff --git a/file/api/v1/zz_generated.deepcopy.go b/file/api/v1/zz_generated.deepcopy.go index 2cf2f20dc..fc644dcbd 100644 --- a/file/api/v1/zz_generated.deepcopy.go +++ b/file/api/v1/zz_generated.deepcopy.go @@ -49,6 +49,46 @@ func (in *PublicKey) DeepCopy() *PublicKey { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SftpExposure) DeepCopyInto(out *SftpExposure) { + *out = *in + if in.PublicKeys != nil { + in, out := &in.PublicKeys, &out.PublicKeys + *out = make([]PublicKey, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SftpExposure. +func (in *SftpExposure) DeepCopy() *SftpExposure { + if in == nil { + return nil + } + out := new(SftpExposure) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SftpSubscription) DeepCopyInto(out *SftpSubscription) { + *out = *in + if in.PublicKeys != nil { + in, out := &in.PublicKeys, &out.PublicKeys + *out = make([]PublicKey, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SftpSubscription. +func (in *SftpSubscription) DeepCopy() *SftpSubscription { + if in == nil { + return nil + } + out := new(SftpSubscription) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FileTypeSpec) DeepCopyInto(out *FileTypeSpec) { *out = *in @@ -149,11 +189,7 @@ func (in *FileTypeList) DeepCopyObject() runtime.Object { func (in *FileExposureSpec) DeepCopyInto(out *FileExposureSpec) { *out = *in in.Approval.DeepCopyInto(&out.Approval) - if in.PublicKeys != nil { - in, out := &in.PublicKeys, &out.PublicKeys - *out = make([]PublicKey, len(*in)) - copy(*out, *in) - } + in.Sftp.DeepCopyInto(&out.Sftp) in.Zone.DeepCopyInto(&out.Zone) } @@ -258,11 +294,7 @@ func (in *FileExposureList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FileSubscriptionSpec) DeepCopyInto(out *FileSubscriptionSpec) { *out = *in - if in.PublicKeys != nil { - in, out := &in.PublicKeys, &out.PublicKeys - *out = make([]PublicKey, len(*in)) - copy(*out, *in) - } + in.Sftp.DeepCopyInto(&out.Sftp) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSubscriptionSpec. diff --git a/file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml b/file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml index 6e8d63c6a..5badf8853 100644 --- a/file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml +++ b/file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml @@ -78,29 +78,38 @@ spec: References the FileType CR via MakeFileTypeName() conversion. minLength: 1 type: string - publicKeys: - description: PublicKeys are the SSH public keys registered for the - provider's SFTP user. - items: - description: PublicKey is a labeled SSH public key registered on - the SFTP user. - properties: - key: - description: Key is the SSH public key value. It must be unique - per file type. - minLength: 1 - type: string - label: - description: Label is a human-readable identifier for the key. - It must be unique per file type. - minLength: 1 - type: string - required: - - key - - label - type: object - minItems: 1 - type: array + sftp: + description: |- + Sftp holds the SFTP storage-backend-specific configuration for this exposure. + Backend-specific settings live under their own sub-object (e.g. sftp) so that + additional storage backends can be added without polluting the spec root. + properties: + publicKeys: + description: PublicKeys are the SSH public keys registered for + the provider's SFTP user. + items: + description: PublicKey is a labeled SSH public key registered + on the SFTP user. + properties: + key: + description: Key is the SSH public key value. It must be + unique per file type. + minLength: 1 + type: string + label: + description: Label is a human-readable identifier for the + key. It must be unique per file type. + minLength: 1 + type: string + required: + - key + - label + type: object + minItems: 1 + type: array + required: + - publicKeys + type: object visibility: default: Enterprise description: Visibility defines who can see and subscribe to this file @@ -129,7 +138,7 @@ spec: required: - approval - fileType - - publicKeys + - sftp - zone type: object status: diff --git a/file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml b/file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml index 1ff07fddc..22f793631 100644 --- a/file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml +++ b/file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml @@ -48,32 +48,45 @@ spec: References the FileType CR via MakeFileTypeName() conversion. minLength: 1 type: string - publicKeys: - description: PublicKeys are the SSH public keys registered for the - consumer's SFTP user. - items: - description: PublicKey is a labeled SSH public key registered on - the SFTP user. - properties: - key: - description: Key is the SSH public key value. It must be unique - per file type. - minLength: 1 - type: string - label: - description: Label is a human-readable identifier for the key. - It must be unique per file type. - minLength: 1 - type: string - required: - - key - - label - type: object - minItems: 1 - type: array + sftp: + description: |- + Sftp holds the SFTP storage-backend-specific configuration for this subscription. + Backend-specific settings live under their own sub-object (e.g. sftp) so that + additional storage backends can be added without polluting the spec root. + properties: + clientId: + description: ClientId identifies the consumer application's client + on the SFTP backend. + type: string + publicKeys: + description: PublicKeys are the SSH public keys registered for + the consumer's SFTP user. + items: + description: PublicKey is a labeled SSH public key registered + on the SFTP user. + properties: + key: + description: Key is the SSH public key value. It must be + unique per file type. + minLength: 1 + type: string + label: + description: Label is a human-readable identifier for the + key. It must be unique per file type. + minLength: 1 + type: string + required: + - key + - label + type: object + minItems: 1 + type: array + required: + - publicKeys + type: object required: - fileType - - publicKeys + - sftp type: object status: description: FileSubscriptionStatus defines the observed state of FileSubscription. diff --git a/rover-server/internal/mapper/rover/out/exposure.go b/rover-server/internal/mapper/rover/out/exposure.go index 1b26ba0bb..2661c41d8 100644 --- a/rover-server/internal/mapper/rover/out/exposure.go +++ b/rover-server/internal/mapper/rover/out/exposure.go @@ -38,12 +38,40 @@ func mapExposure(in *roverv1.Exposure, out *api.Exposure) error { return errors.Wrap(err, "failed to map event exposure") } + } else if in.File != nil { + if err := out.FromFileExposure(mapFileExposure(in.File)); err != nil { + return errors.Wrap(err, "failed to map file exposure") + } + } else { return errors.Errorf("unknown exposure type: %s", in.Type()) } return nil } +func mapFileExposure(in *roverv1.FileExposure) api.FileExposure { + return api.FileExposure{ + FileType: in.FileType, + Variant: api.FileExposureVariant(in.Variant), + Visibility: toApiVisibility(in.Visibility), + PublicKeys: mapPublicKeys(in.PublicKeys), + } +} + +func mapPublicKeys(in []roverv1.PublicKey) []api.PublicKey { + if len(in) == 0 { + return nil + } + out := make([]api.PublicKey, len(in)) + for i, key := range in { + out[i] = api.PublicKey{ + Label: key.Label, + Key: key.Key, + } + } + return out +} + func mapApiExposure(in *roverv1.ApiExposure) api.ApiExposure { apiExposure := api.ApiExposure{ BasePath: in.BasePath, diff --git a/rover/api/v1/rover_types.go b/rover/api/v1/rover_types.go index 0ccf00ccd..99f114a33 100644 --- a/rover/api/v1/rover_types.go +++ b/rover/api/v1/rover_types.go @@ -5,8 +5,6 @@ package v1 import ( - "slices" - "github.com/telekom/controlplane/common/pkg/types" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -212,16 +210,6 @@ const ( FileVariantSFTP FileVariant = "sftp" ) -// SupportedFileTypeZones lists the zones on which file type (SFTP) exposures and -// subscriptions are currently supported. CloudWalker has been retired, so SFTP file -// types are limited to the external zones cetus and canis. -var SupportedFileTypeZones = []string{"cetus", "canis"} - -// IsFileTypeZoneSupported reports whether the given zone supports file types (SFTP). -func IsFileTypeZoneSupported(zone string) bool { - return slices.Contains(SupportedFileTypeZones, zone) -} - // ApprovalStrategy defines the approval workflow for API exposure type ApprovalStrategy string diff --git a/rover/api/v1/rover_types_test.go b/rover/api/v1/rover_types_test.go index cae38bb30..0d812854c 100644 --- a/rover/api/v1/rover_types_test.go +++ b/rover/api/v1/rover_types_test.go @@ -400,14 +400,6 @@ var _ = Describe("Rover V1 Test Suite", func() { Expect(sub.Type()).To(Equal(v1.TypeFile)) }) - It("should only support file types on the cetus and canis zones", func() { - Expect(v1.IsFileTypeZoneSupported("cetus")).To(BeTrue()) - Expect(v1.IsFileTypeZoneSupported("canis")).To(BeTrue()) - Expect(v1.IsFileTypeZoneSupported("aws")).To(BeFalse()) - Expect(v1.IsFileTypeZoneSupported("gaia")).To(BeFalse()) - Expect(v1.IsFileTypeZoneSupported("")).To(BeFalse()) - }) - It("should accept a Rover with a file type exposure and subscription", func() { rover := new(v1.Rover) rover.Name = "file-rover" diff --git a/rover/internal/controller/rover_controller.go b/rover/internal/controller/rover_controller.go index 374c37837..f4328eac5 100644 --- a/rover/internal/controller/rover_controller.go +++ b/rover/internal/controller/rover_controller.go @@ -55,6 +55,9 @@ type RoverReconciler struct { // +kubebuilder:rbac:groups=event.cp.ei.telekom.de,resources=eventexposures,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=event.cp.ei.telekom.de,resources=eventsubscriptions,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=file.ei.telekom.de,resources=fileexposures,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=file.ei.telekom.de,resources=filesubscriptions,verbs=get;list;watch;create;update;patch;delete + // +kubebuilder:rbac:groups=permission.cp.ei.telekom.de,resources=permissionsets,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=application.cp.ei.telekom.de,resources=applications,verbs=get;list;watch;create;update;patch;delete diff --git a/rover/internal/handler/rover/file/exposure.go b/rover/internal/handler/rover/file/exposure.go index 932e99899..47342d9f5 100644 --- a/rover/internal/handler/rover/file/exposure.go +++ b/rover/internal/handler/rover/file/exposure.go @@ -55,8 +55,10 @@ func HandleExposure(ctx context.Context, c client.JanitorClient, owner *roverv1. Approval: filev1.Approval{Strategy: filev1.ApprovalStrategySimple}, Visibility: filev1.Visibility(exp.Visibility.String()), FileType: exp.FileType, - PublicKeys: mapPublicKeys(exp.PublicKeys), - Zone: zoneRef, + Sftp: filev1.SftpExposure{ + PublicKeys: mapPublicKeys(exp.PublicKeys), + }, + Zone: zoneRef, } return nil } diff --git a/rover/internal/handler/rover/file/subscription.go b/rover/internal/handler/rover/file/subscription.go index de9bf5387..a3e87df5c 100644 --- a/rover/internal/handler/rover/file/subscription.go +++ b/rover/internal/handler/rover/file/subscription.go @@ -45,8 +45,10 @@ func HandleSubscription(ctx context.Context, c client.JanitorClient, owner *rove } fileSubscription.Spec = filev1.FileSubscriptionSpec{ - FileType: sub.FileType, - PublicKeys: mapPublicKeys(sub.PublicKeys), + FileType: sub.FileType, + Sftp: filev1.SftpSubscription{ + PublicKeys: mapPublicKeys(sub.PublicKeys), + }, } return nil } diff --git a/rover/internal/webhook/v1/filespecification_webhook_test.go b/rover/internal/webhook/v1/filespecification_webhook_test.go index f11fa547f..f312ebe94 100644 --- a/rover/internal/webhook/v1/filespecification_webhook_test.go +++ b/rover/internal/webhook/v1/filespecification_webhook_test.go @@ -106,24 +106,6 @@ var _ = Describe("File Type (SFTP) Validation", func() { }} } - It("should reject a file exposure on an unsupported zone", func() { - // testZone is named "test" and is not in {cetus, canis}. - rover := NewRover(testZone) - rover.Spec.Exposures = []roverv1.Exposure{fileExposure()} - warnings, err := validator.ValidateCreate(ctx, rover) - assertValidationFailedWith(warnings, err, "does not support file types") - }) - - It("should reject a file subscription on an unsupported zone", func() { - rover := NewRover(testZone) - rover.Spec.Subscriptions = []roverv1.Subscription{{File: &roverv1.FileSubscription{ - FileType: "demo-sftp-spec-v1", - PublicKeys: []roverv1.PublicKey{{Label: "consumer-key", Key: "ssh-ed25519 BBBB"}}, - }}} - warnings, err := validator.ValidateCreate(ctx, rover) - assertValidationFailedWith(warnings, err, "does not support file types") - }) - It("should accept a file exposure on a supported zone (cetus)", func() { cetus := NewZone("cetus", testZone.Namespace) CreateZone(ctx, cetus) diff --git a/rover/internal/webhook/v1/rover_webhook.go b/rover/internal/webhook/v1/rover_webhook.go index 58082327f..51424cf82 100644 --- a/rover/internal/webhook/v1/rover_webhook.go +++ b/rover/internal/webhook/v1/rover_webhook.go @@ -200,20 +200,6 @@ func (r *RoverValidator) ValidateCreateOrUpdate(ctx context.Context, rover *rove } } - subscribesToFiles := slices.ContainsFunc(rover.Spec.Subscriptions, func(sub roverv1.Subscription) bool { - return sub.Type() == roverv1.TypeFile - }) - exposesFiles := slices.ContainsFunc(rover.Spec.Exposures, func(exp roverv1.Exposure) bool { - return exp.Type() == roverv1.TypeFile - }) - if (subscribesToFiles || exposesFiles) && !roverv1.IsFileTypeZoneSupported(rover.Spec.Zone) { - valErr.AddInvalidError( - field.NewPath("spec").Child("zone"), - rover.Spec.Zone, - fmt.Sprintf("zone '%s' does not support file types; only %s are supported", rover.Spec.Zone, strings.Join(roverv1.SupportedFileTypeZones, ", ")), - ) - } - if err := MustNotHaveDuplicates(valErr, rover.Spec.Subscriptions, rover.Spec.Exposures); err != nil { return nil, err } From 924b7cc6ece8b49ee30933c44026e1c40b789aec Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Wed, 22 Jul 2026 18:22:57 +0300 Subject: [PATCH 12/31] feat(sftp integration): dhei-20905 delete . "github.com/onsi/gomega/gstruct" lib as it was added by AI add crypto lib and ssh pub key validation --- rover/api/v1/roadmap_types_test.go | 15 ---- rover/api/v1/rover_types.go | 28 ++++++ rover/go.mod | 1 + rover/go.sum | 2 + .../v1/filespecification_webhook_test.go | 88 +++++++++++++++++-- rover/internal/webhook/v1/rover_webhook.go | 29 ++++++ 6 files changed, 141 insertions(+), 22 deletions(-) diff --git a/rover/api/v1/roadmap_types_test.go b/rover/api/v1/roadmap_types_test.go index d61dea3b3..b2b57c53c 100644 --- a/rover/api/v1/roadmap_types_test.go +++ b/rover/api/v1/roadmap_types_test.go @@ -7,7 +7,6 @@ package v1_test import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - . "github.com/onsi/gomega/gstruct" "github.com/telekom/controlplane/common/pkg/types" v1 "github.com/telekom/controlplane/rover/api/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -70,9 +69,6 @@ var _ = Describe("Roadmap V1 Test Suite", func() { Expect(ok).To(BeTrue()) Expect(statusErr.Status().Reason).To(Equal(metav1.StatusReasonInvalid)) - Expect(statusErr.Status().Details.Causes).To(ContainElement(MatchFields(IgnoreExtras, Fields{ - "Field": Equal("spec.specificationRef.name"), - }))) }) It("should reject a Roadmap with empty specification namespace", func() { @@ -102,9 +98,6 @@ var _ = Describe("Roadmap V1 Test Suite", func() { Expect(ok).To(BeTrue()) Expect(statusErr.Status().Reason).To(Equal(metav1.StatusReasonInvalid)) - Expect(statusErr.Status().Details.Causes).To(ContainElement(MatchFields(IgnoreExtras, Fields{ - "Field": Equal("spec.specificationRef.namespace"), - }))) }) It("should reject a Roadmap with empty contents file ID", func() { @@ -134,10 +127,6 @@ var _ = Describe("Roadmap V1 Test Suite", func() { Expect(ok).To(BeTrue()) Expect(statusErr.Status().Reason).To(Equal(metav1.StatusReasonInvalid)) - // The error message might vary, but field should be spec.contents - Expect(statusErr.Status().Details.Causes).To(ContainElement(MatchFields(IgnoreExtras, Fields{ - "Field": Equal("spec.contents"), - }))) }) It("should reject a Roadmap with empty hash", func() { @@ -167,10 +156,6 @@ var _ = Describe("Roadmap V1 Test Suite", func() { Expect(ok).To(BeTrue()) Expect(statusErr.Status().Reason).To(Equal(metav1.StatusReasonInvalid)) - // The error message might vary, but field should be spec.hash - Expect(statusErr.Status().Details.Causes).To(ContainElement(MatchFields(IgnoreExtras, Fields{ - "Field": Equal("spec.hash"), - }))) }) }) }) diff --git a/rover/api/v1/rover_types.go b/rover/api/v1/rover_types.go index 99f114a33..bb699bba0 100644 --- a/rover/api/v1/rover_types.go +++ b/rover/api/v1/rover_types.go @@ -499,6 +499,34 @@ type PublicKey struct { Key string `json:"key"` } +// SSHKeyType identifies the algorithm prefix of an SSH public key registered on +// a SFTP user. Only these algorithms are accepted for file exposures and subscriptions. +type SSHKeyType string + +const ( + SSHKeyTypeRSA SSHKeyType = "ssh-rsa" + SSHKeyTypeECDSANistP521 SSHKeyType = "ecdsa-sha2-nistp521" + SSHKeyTypeED25519 SSHKeyType = "ssh-ed25519" +) + +var AllSSHKeyTypes = []SSHKeyType{ + SSHKeyTypeRSA, + SSHKeyTypeECDSANistP521, + SSHKeyTypeED25519, +} + +func (t SSHKeyType) String() string { + return string(t) +} + +func (t SSHKeyType) IsValid() bool { + switch t { + case SSHKeyTypeRSA, SSHKeyTypeECDSANistP521, SSHKeyTypeED25519: + return true + } + return false +} + // Approval defines the approval workflow for API exposure type Approval struct { // Strategy defines the approval process required for this API diff --git a/rover/go.mod b/rover/go.mod index 693036da7..5781b4657 100644 --- a/rover/go.mod +++ b/rover/go.mod @@ -26,6 +26,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.11.1 github.com/telekom/controlplane/file/api v0.0.0-00010101000000-000000000000 + golang.org/x/crypto v0.52.0 k8s.io/api v0.36.0 k8s.io/apimachinery v0.36.0 k8s.io/client-go v0.36.0 diff --git a/rover/go.sum b/rover/go.sum index dc64f7547..d009149ab 100644 --- a/rover/go.sum +++ b/rover/go.sum @@ -203,6 +203,8 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= diff --git a/rover/internal/webhook/v1/filespecification_webhook_test.go b/rover/internal/webhook/v1/filespecification_webhook_test.go index f312ebe94..260543ac9 100644 --- a/rover/internal/webhook/v1/filespecification_webhook_test.go +++ b/rover/internal/webhook/v1/filespecification_webhook_test.go @@ -5,8 +5,17 @@ package v1 import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "strings" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "golang.org/x/crypto/ssh" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" @@ -34,8 +43,8 @@ var _ = Describe("File Type (SFTP) Validation", func() { It("should accept unique labels and key values", func() { valErr := newValErr() keys := []roverv1.PublicKey{ - {Label: "provider-key", Key: "ssh-ed25519 AAAA1"}, - {Label: "consumer-key", Key: "ssh-ed25519 AAAA2"}, + {Label: "provider-key", Key: newED25519Key()}, + {Label: "consumer-key", Key: newED25519Key()}, } validateFilePublicKeys(valErr, keys, filePath) Expect(valErr.BuildError()).NotTo(HaveOccurred()) @@ -44,8 +53,8 @@ var _ = Describe("File Type (SFTP) Validation", func() { It("should reject duplicate public key labels per fileType", func() { valErr := newValErr() keys := []roverv1.PublicKey{ - {Label: "dup", Key: "ssh-ed25519 AAAA1"}, - {Label: "dup", Key: "ssh-ed25519 AAAA2"}, + {Label: "dup", Key: newED25519Key()}, + {Label: "dup", Key: newED25519Key()}, } validateFilePublicKeys(valErr, keys, filePath) err := valErr.BuildError() @@ -55,15 +64,51 @@ var _ = Describe("File Type (SFTP) Validation", func() { It("should reject duplicate public key values per fileType", func() { valErr := newValErr() + sameKey := newED25519Key() keys := []roverv1.PublicKey{ - {Label: "key-a", Key: "ssh-ed25519 SAME"}, - {Label: "key-b", Key: "ssh-ed25519 SAME"}, + {Label: "key-a", Key: sameKey}, + {Label: "key-b", Key: sameKey}, } validateFilePublicKeys(valErr, keys, filePath) err := valErr.BuildError() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("key values must be unique per fileType")) }) + + It("should accept all supported SSH key types", func() { + valErr := newValErr() + keys := []roverv1.PublicKey{ + {Label: "rsa-key", Key: newRSAKey()}, + {Label: "ed25519-key", Key: newED25519Key()}, + {Label: "ecdsa-key", Key: newECDSAKey(elliptic.P521())}, + } + validateFilePublicKeys(valErr, keys, filePath) + Expect(valErr.BuildError()).NotTo(HaveOccurred()) + }) + + It("should reject a malformed key that cannot be parsed", func() { + valErr := newValErr() + keys := []roverv1.PublicKey{ + {Label: "bad-key", Key: "ssh-ed25519 not-valid-base64!!"}, + } + validateFilePublicKeys(valErr, keys, filePath) + err := valErr.BuildError() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("invalid SSH public key for label 'bad-key'")) + }) + + It("should reject a well-formed key of an unsupported type", func() { + valErr := newValErr() + keys := []roverv1.PublicKey{ + // A valid ECDSA P-256 key parses fine but is not in the allowlist + // (only ecdsa-sha2-nistp521 is supported). + {Label: "bad-type", Key: newECDSAKey(elliptic.P256())}, + } + validateFilePublicKeys(valErr, keys, filePath) + err := valErr.BuildError() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unsupported key type 'ecdsa-sha2-nistp256' for key labelled 'bad-type'")) + }) }) Context("MustNotHaveDuplicates for file types", func() { @@ -102,7 +147,7 @@ var _ = Describe("File Type (SFTP) Validation", func() { fileExposure := func() roverv1.Exposure { return roverv1.Exposure{File: &roverv1.FileExposure{ FileType: "demo-sftp-spec-v1", - PublicKeys: []roverv1.PublicKey{{Label: "provider-key", Key: "ssh-ed25519 AAAA"}}, + PublicKeys: []roverv1.PublicKey{{Label: "provider-key", Key: newED25519Key()}}, }} } @@ -150,3 +195,32 @@ var _ = Describe("File Type (SFTP) Validation", func() { }) }) }) + +// mustAuthorizedKey marshals a crypto public key into an SSH authorized-keys line. +func mustAuthorizedKey(pub crypto.PublicKey) string { + sshPub, err := ssh.NewPublicKey(pub) + Expect(err).NotTo(HaveOccurred()) + return strings.TrimSpace(string(ssh.MarshalAuthorizedKey(sshPub))) +} + +// newED25519Key generates a fresh, valid ssh-ed25519 authorized-keys entry. +func newED25519Key() string { + pub, _, err := ed25519.GenerateKey(rand.Reader) + Expect(err).NotTo(HaveOccurred()) + return mustAuthorizedKey(pub) +} + +// newRSAKey generates a fresh, valid ssh-rsa authorized-keys entry. +func newRSAKey() string { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + Expect(err).NotTo(HaveOccurred()) + return mustAuthorizedKey(priv.Public()) +} + +// newECDSAKey generates a fresh, valid ecdsa-sha2-* authorized-keys entry for the +// given curve (e.g. elliptic.P521() -> ecdsa-sha2-nistp521). +func newECDSAKey(curve elliptic.Curve) string { + priv, err := ecdsa.GenerateKey(curve, rand.Reader) + Expect(err).NotTo(HaveOccurred()) + return mustAuthorizedKey(priv.Public()) +} diff --git a/rover/internal/webhook/v1/rover_webhook.go b/rover/internal/webhook/v1/rover_webhook.go index 51424cf82..3ce557563 100644 --- a/rover/internal/webhook/v1/rover_webhook.go +++ b/rover/internal/webhook/v1/rover_webhook.go @@ -20,6 +20,7 @@ import ( eventv1 "github.com/telekom/controlplane/event/api/v1" organizationv1 "github.com/telekom/controlplane/organization/api/v1" roverv1 "github.com/telekom/controlplane/rover/api/v1" + "golang.org/x/crypto/ssh" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" @@ -651,5 +652,33 @@ func validateFilePublicKeys(valErr *cerrors.ValidationError, keys []roverv1.Publ ) } seenKeys[key.Key] = struct{}{} + + validateSSHPublicKeyFormat(valErr, key, keyPath) + } +} + +// validateSSHPublicKeyFormat verifies that a public key value is a well-formed +// SSH authorized-keys entry (" [comment]") whose algorithm is +// one of the supported SSHKeyTypes. +func validateSSHPublicKeyFormat(valErr *cerrors.ValidationError, key roverv1.PublicKey, keyPath *field.Path) { + pub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(key.Key))) + if err != nil { + valErr.AddInvalidError( + keyPath.Child("key"), + key.Key, + fmt.Sprintf("invalid SSH public key for label '%s': %v", key.Label, err), + ) + return + } + + if !roverv1.SSHKeyType(pub.Type()).IsValid() { + valErr.AddInvalidError( + keyPath.Child("key"), + key.Key, + fmt.Sprintf( + "unsupported key type '%s' for key labelled '%s'; must be one of %v", + pub.Type(), key.Label, roverv1.AllSSHKeyTypes, + ), + ) } } From 0117241dd24616e667d2ddf462ca97733480950f Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Thu, 23 Jul 2026 17:31:01 +0300 Subject: [PATCH 13/31] feat(sftp integration): fix hardcoded approval strategy --- rover/api/v1/rover_types.go | 4 ++++ rover/api/v1/zz_generated.deepcopy.go | 1 + rover/internal/handler/rover/file/exposure.go | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/rover/api/v1/rover_types.go b/rover/api/v1/rover_types.go index bb699bba0..d576a22d6 100644 --- a/rover/api/v1/rover_types.go +++ b/rover/api/v1/rover_types.go @@ -456,6 +456,10 @@ type FileExposure struct { // +kubebuilder:default=Enterprise Visibility Visibility `json:"visibility"` + // Approval defines the approval workflow required for subscriptions to this file type + // +kubebuilder:validation:Required + Approval Approval `json:"approval"` + // PublicKeys are the SSH public keys registered for the producer's SFTP user. // At least one key is required. Both label and key value must be unique per fileType. // +kubebuilder:validation:Required diff --git a/rover/api/v1/zz_generated.deepcopy.go b/rover/api/v1/zz_generated.deepcopy.go index b587de318..7aed0376f 100644 --- a/rover/api/v1/zz_generated.deepcopy.go +++ b/rover/api/v1/zz_generated.deepcopy.go @@ -740,6 +740,7 @@ func (in *ExternalIdentityProvider) DeepCopy() *ExternalIdentityProvider { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FileExposure) DeepCopyInto(out *FileExposure) { *out = *in + in.Approval.DeepCopyInto(&out.Approval) if in.PublicKeys != nil { in, out := &in.PublicKeys, &out.PublicKeys *out = make([]PublicKey, len(*in)) diff --git a/rover/internal/handler/rover/file/exposure.go b/rover/internal/handler/rover/file/exposure.go index 47342d9f5..b8c3bc6e8 100644 --- a/rover/internal/handler/rover/file/exposure.go +++ b/rover/internal/handler/rover/file/exposure.go @@ -52,7 +52,7 @@ func HandleExposure(ctx context.Context, c client.JanitorClient, owner *roverv1. } fileExposure.Spec = filev1.FileExposureSpec{ - Approval: filev1.Approval{Strategy: filev1.ApprovalStrategySimple}, + Approval: filev1.Approval{Strategy: filev1.ApprovalStrategy(exp.Approval.Strategy)}, Visibility: filev1.Visibility(exp.Visibility.String()), FileType: exp.FileType, Sftp: filev1.SftpExposure{ From 9c10b72c92523c9738606410cd439b9f3f10a4c1 Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Fri, 24 Jul 2026 13:55:48 +0300 Subject: [PATCH 14/31] feat(sftp integration): delete the variant proccessing rename func --- rover-server/api/openapi.yaml | 13 +++----- .../internal/mapper/rover/in/exposure.go | 1 - .../internal/mapper/rover/in/file_test.go | 19 ------------ .../internal/mapper/rover/in/subscription.go | 1 - .../internal/mapper/rover/out/exposure.go | 1 - rover/api/v1/rover_types.go | 31 ------------------- rover/api/v1/rover_types_test.go | 1 - .../bases/rover.cp.ei.telekom.de_rovers.yaml | 7 ----- 8 files changed, 4 insertions(+), 70 deletions(-) diff --git a/rover-server/api/openapi.yaml b/rover-server/api/openapi.yaml index d433dd013..686c90af8 100644 --- a/rover-server/api/openapi.yaml +++ b/rover-server/api/openapi.yaml @@ -301,7 +301,7 @@ paths: description: > **Important:** The deletion is done asynchronously to ensure that the resource was actually deleted, use this or the GET resource until you - receive a 404 response. + receive a 404 response. Delete Rover - will remove your API / Subscriptions / Exposures operationId: deleteRover @@ -733,7 +733,7 @@ paths: description: > **Important:** The deletion is done asynchronously to ensure that the resource was actually deleted, use this or the GET resource until you - receive a 404 response. + receive a 404 response. Delete an ApiSpecification operationId: deleteApiSpecification @@ -969,7 +969,7 @@ paths: description: > **Important:** The deletion is done asynchronously to ensure that the resource was actually deleted, use this or the GET resource until you - receive a 404 response. + receive a 404 response. Delete an EventSpecification operationId: deleteEventSpecification @@ -2070,7 +2070,7 @@ components: - id allOf: - $ref: '#/components/schemas/RateLimit' - - properties: + - properties: id: type: string description: The unique ID of this consumer (their clientId) @@ -2467,11 +2467,6 @@ components: type: string fileType: type: string - variant: - type: string - enum: - - sftp - description: File-transfer backend. Optional; currently only "sftp" is supported. publicKeys: type: array items: diff --git a/rover-server/internal/mapper/rover/in/exposure.go b/rover-server/internal/mapper/rover/in/exposure.go index 08f64a3f8..5d69ecc0f 100644 --- a/rover-server/internal/mapper/rover/in/exposure.go +++ b/rover-server/internal/mapper/rover/in/exposure.go @@ -71,7 +71,6 @@ func mapExposure(in *api.Exposure, out *roverv1.Exposure) error { func mapFileExposure(in api.FileExposure) *roverv1.FileExposure { out := &roverv1.FileExposure{ FileType: in.FileType, - Variant: roverv1.FileVariant(in.Variant), Visibility: toRoverVisibility(in.Visibility), PublicKeys: mapPublicKeys(in.PublicKeys), } diff --git a/rover-server/internal/mapper/rover/in/file_test.go b/rover-server/internal/mapper/rover/in/file_test.go index 62cec9fdf..02540a36e 100644 --- a/rover-server/internal/mapper/rover/in/file_test.go +++ b/rover-server/internal/mapper/rover/in/file_test.go @@ -19,7 +19,6 @@ var _ = Describe("File Type (SFTP) Mapper", func() { input := api.FileExposure{ Type: "file", FileType: "demo-sftp-spec-v1", - Variant: "sftp", Visibility: api.WORLD, PublicKeys: []api.PublicKey{ {Label: "provider-key", Key: "ssh-ed25519 AAAA1"}, @@ -30,7 +29,6 @@ var _ = Describe("File Type (SFTP) Mapper", func() { Expect(output).ToNot(BeNil()) Expect(output.FileType).To(Equal("demo-sftp-spec-v1")) - Expect(output.Variant).To(Equal(roverv1.FileVariantSFTP)) Expect(output.Visibility).To(Equal(roverv1.VisibilityWorld)) Expect(output.PublicKeys).To(HaveLen(1)) Expect(output.PublicKeys[0].Label).To(Equal("provider-key")) @@ -53,21 +51,6 @@ var _ = Describe("File Type (SFTP) Mapper", func() { Expect(output.Visibility).To(BeEmpty()) }) - - It("must leave variant empty when omitted (optional field)", func() { - input := api.FileExposure{ - Type: "file", - FileType: "demo-sftp-spec-v1", - Visibility: api.WORLD, - PublicKeys: []api.PublicKey{ - {Label: "provider-key", Key: "ssh-ed25519 AAAA1"}, - }, - } - - output := mapFileExposure(input) - - Expect(output.Variant).To(BeEmpty()) - }) }) Context("mapFileSubscription", func() { @@ -75,7 +58,6 @@ var _ = Describe("File Type (SFTP) Mapper", func() { input := api.FileSubscription{ Type: "file", FileType: "demo-sftp-spec-v1", - Variant: "sftp", PublicKeys: []api.PublicKey{ {Label: "consumer-key", Key: "ssh-ed25519 AAAA2"}, }, @@ -85,7 +67,6 @@ var _ = Describe("File Type (SFTP) Mapper", func() { Expect(output).ToNot(BeNil()) Expect(output.FileType).To(Equal("demo-sftp-spec-v1")) - Expect(output.Variant).To(Equal(roverv1.FileVariantSFTP)) Expect(output.PublicKeys).To(HaveLen(1)) Expect(output.PublicKeys[0].Label).To(Equal("consumer-key")) Expect(output.PublicKeys[0].Key).To(Equal("ssh-ed25519 AAAA2")) diff --git a/rover-server/internal/mapper/rover/in/subscription.go b/rover-server/internal/mapper/rover/in/subscription.go index 552f45d5d..535e346b0 100644 --- a/rover-server/internal/mapper/rover/in/subscription.go +++ b/rover-server/internal/mapper/rover/in/subscription.go @@ -54,7 +54,6 @@ func mapSubscription(in *api.Subscription, out *roverv1.Subscription) error { func mapFileSubscription(in api.FileSubscription) *roverv1.FileSubscription { return &roverv1.FileSubscription{ FileType: in.FileType, - Variant: roverv1.FileVariant(in.Variant), PublicKeys: mapPublicKeys(in.PublicKeys), } } diff --git a/rover-server/internal/mapper/rover/out/exposure.go b/rover-server/internal/mapper/rover/out/exposure.go index 2661c41d8..b932f05ae 100644 --- a/rover-server/internal/mapper/rover/out/exposure.go +++ b/rover-server/internal/mapper/rover/out/exposure.go @@ -52,7 +52,6 @@ func mapExposure(in *roverv1.Exposure, out *api.Exposure) error { func mapFileExposure(in *roverv1.FileExposure) api.FileExposure { return api.FileExposure{ FileType: in.FileType, - Variant: api.FileExposureVariant(in.Variant), Visibility: toApiVisibility(in.Visibility), PublicKeys: mapPublicKeys(in.PublicKeys), } diff --git a/rover/api/v1/rover_types.go b/rover/api/v1/rover_types.go index d576a22d6..55906ad05 100644 --- a/rover/api/v1/rover_types.go +++ b/rover/api/v1/rover_types.go @@ -191,25 +191,6 @@ const ( TypeFile Type = "file" ) -// FileVariant selects the file-transfer backend used for a file type exposure or -// subscription. Currently only SFTP is supported, as CloudWalker has been retired. -type FileVariant string - -// String returns the raw string value of the FileVariant. -// -// TODO(DHEI-20903): currently unused in production code (only asserted in tests). -// It will be called by the file-domain handler -// (rover/internal/handler/rover/file, added in DHEI-20903) when logging/serialising -// the selected variant while creating the file-domain CRD. -func (v FileVariant) String() string { - return string(v) -} - -const ( - // FileVariantSFTP indicates that the file type is handled via the SFTP backend. - FileVariantSFTP FileVariant = "sftp" -) - // ApprovalStrategy defines the approval workflow for API exposure type ApprovalStrategy string @@ -445,12 +426,6 @@ type FileExposure struct { // +kubebuilder:validation:MinLength=1 FileType string `json:"fileType"` - // Variant selects the file-transfer backend. Currently only "sftp" is - // supported. The field is optional since CloudWalker has been retired. - // +kubebuilder:validation:Optional - // +kubebuilder:validation:Enum=sftp - Variant FileVariant `json:"variant,omitempty"` - // Visibility defines who can see and subscribe to this file type // +kubebuilder:validation:Enum=World;Zone;Enterprise // +kubebuilder:default=Enterprise @@ -477,12 +452,6 @@ type FileSubscription struct { // +kubebuilder:validation:MinLength=1 FileType string `json:"fileType"` - // Variant selects the file-transfer backend. Currently only "sftp" is - // supported. The field is optional since CloudWalker has been retired. - // +kubebuilder:validation:Optional - // +kubebuilder:validation:Enum=sftp - Variant FileVariant `json:"variant,omitempty"` - // PublicKeys are the SSH public keys registered for the consumer's SFTP user. // At least one key is required. Both label and key value must be unique per fileType. // +kubebuilder:validation:Required diff --git a/rover/api/v1/rover_types_test.go b/rover/api/v1/rover_types_test.go index 0d812854c..86186345f 100644 --- a/rover/api/v1/rover_types_test.go +++ b/rover/api/v1/rover_types_test.go @@ -411,7 +411,6 @@ var _ = Describe("Rover V1 Test Suite", func() { { File: &v1.FileExposure{ FileType: "demo-sftp-spec-v1", - Variant: v1.FileVariantSFTP, Visibility: v1.VisibilityWorld, PublicKeys: []v1.PublicKey{ {Label: "demo-provider-key", Key: "ssh-ed25519 AAAAprovider"}, diff --git a/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml b/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml index 88eafac46..70af63db1 100644 --- a/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml +++ b/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml @@ -1091,13 +1091,6 @@ spec: type: object minItems: 1 type: array - variant: - description: |- - Variant selects the file-transfer backend. Currently only "sftp" is - supported. - enum: - - sftp - type: string required: - fileType - publicKeys From 7bab1991a353770d22e0a92ce99cef8ae6aefcb9 Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Fri, 24 Jul 2026 13:56:28 +0300 Subject: [PATCH 15/31] feat(sftp integration): delete the variant proccessing rename func --- rover/internal/handler/rover/application/application.go | 6 +++--- .../internal/handler/rover/application/application_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rover/internal/handler/rover/application/application.go b/rover/internal/handler/rover/application/application.go index 4dcef5e73..266e2da8b 100644 --- a/rover/internal/handler/rover/application/application.go +++ b/rover/internal/handler/rover/application/application.go @@ -49,7 +49,7 @@ func HandleApplication(ctx context.Context, c client.JanitorClient, owner *rover } // If the Application publishes any events, we need to create a client for it, even if it doesn't have any subscriptions. // This is because the client is needed to access the publish-route - needsClient := RoverNeedsClient(owner) + needsClient := isClientNeeded(owner) var subscriberFailoverZones []types.ObjectRef if needsClient { for _, subscription := range owner.Spec.Subscriptions { @@ -132,13 +132,13 @@ func HandleApplication(ctx context.Context, c client.JanitorClient, owner *rover return err } -// RoverNeedsClient reports whether the Application derived from the given Rover +// isClientNeeded reports whether the Application derived from the given Rover // requires an Identity client (and Gateway consumer). // // File-type (SFTP) subscriptions and exposures are realized in the file domain via // SFTP users and SSH public keys. They neither need an Identity client nor a Gateway // consumer. Only non-file subscriptions or any event exposure force a client/consumer. -func RoverNeedsClient(owner *roverv1.Rover) bool { +func isClientNeeded(owner *roverv1.Rover) bool { hasAnyEventExposures := slices.ContainsFunc(owner.Spec.Exposures, func(ex roverv1.Exposure) bool { return ex.Type() == roverv1.TypeEvent }) diff --git a/rover/internal/handler/rover/application/application_test.go b/rover/internal/handler/rover/application/application_test.go index 7a1b04991..9e7b9b507 100644 --- a/rover/internal/handler/rover/application/application_test.go +++ b/rover/internal/handler/rover/application/application_test.go @@ -37,7 +37,7 @@ var _ = Describe("RoverNeedsClient", func() { DescribeTable("decides whether the derived Application requires an Identity client", func(exps []roverv1.Exposure, subs []roverv1.Subscription, expected bool) { - Expect(RoverNeedsClient(newRover(exps, subs))).To(Equal(expected)) + Expect(isClientNeeded(newRover(exps, subs))).To(Equal(expected)) }, // Logical Application (file-only or empty) => no client/consumer. Entry("empty rover", nil, nil, false), From bb0e344d41b5cff4659e0590cb96c8fa3da4298a Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Fri, 24 Jul 2026 16:57:55 +0300 Subject: [PATCH 16/31] owner name normalization move func desc to the original func --- .../handler/rover/application/application.go | 13 +++++-------- rover/internal/handler/rover/file/util.go | 4 +++- rover/internal/handler/rover/file/util_test.go | 1 + 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/rover/internal/handler/rover/application/application.go b/rover/internal/handler/rover/application/application.go index 266e2da8b..2bc3d85e8 100644 --- a/rover/internal/handler/rover/application/application.go +++ b/rover/internal/handler/rover/application/application.go @@ -47,8 +47,7 @@ func HandleApplication(ctx context.Context, c client.JanitorClient, owner *rover } else if err != nil { return err } - // If the Application publishes any events, we need to create a client for it, even if it doesn't have any subscriptions. - // This is because the client is needed to access the publish-route + needsClient := isClientNeeded(owner) var subscriberFailoverZones []types.ObjectRef if needsClient { @@ -132,12 +131,10 @@ func HandleApplication(ctx context.Context, c client.JanitorClient, owner *rover return err } -// isClientNeeded reports whether the Application derived from the given Rover -// requires an Identity client (and Gateway consumer). -// -// File-type (SFTP) subscriptions and exposures are realized in the file domain via -// SFTP users and SSH public keys. They neither need an Identity client nor a Gateway -// consumer. Only non-file subscriptions or any event exposure force a client/consumer. +// isClientNeeded reports whether the derived Application requires an Identity +// client (and Gateway consumer). Non-file subscriptions and event exposures +// (which need client access to the publish-route) require one; file-type (SFTP) +// exposures and subscriptions never do, as they are realized in the file domain. func isClientNeeded(owner *roverv1.Rover) bool { hasAnyEventExposures := slices.ContainsFunc(owner.Spec.Exposures, func(ex roverv1.Exposure) bool { return ex.Type() == roverv1.TypeEvent diff --git a/rover/internal/handler/rover/file/util.go b/rover/internal/handler/rover/file/util.go index 25ca2a9f1..78bc9361e 100644 --- a/rover/internal/handler/rover/file/util.go +++ b/rover/internal/handler/rover/file/util.go @@ -7,12 +7,14 @@ package file import ( filev1 "github.com/telekom/controlplane/file/api/v1" roverv1 "github.com/telekom/controlplane/rover/api/v1" + + "github.com/telekom/controlplane/common/pkg/util/labelutil" ) // MakeName generates a deterministic resource name for a file exposure or // subscription: "--" (spec_dcp naming), normalized. func MakeName(fileType, ownerName string) string { - return filev1.MakeFileTypeName(fileType) + "--" + ownerName + return filev1.MakeFileTypeName(fileType) + "--" + labelutil.NormalizeValue(ownerName) } // mapPublicKeys converts rover-domain public keys to file-domain public keys. diff --git a/rover/internal/handler/rover/file/util_test.go b/rover/internal/handler/rover/file/util_test.go index 7ec5b488d..888184017 100644 --- a/rover/internal/handler/rover/file/util_test.go +++ b/rover/internal/handler/rover/file/util_test.go @@ -20,6 +20,7 @@ func TestMakeName(t *testing.T) { {"hyphenated file type", "de-telekom-eni-foo-v1", "provider", "de-telekom-eni-foo-v1--provider"}, {"dotted file type is normalized", "de.telekom.foo.v1", "consumer", "de-telekom-foo-v1--consumer"}, {"mixed case is lowercased", "De.Telekom.V1", "app", "de-telekom-v1--app"}, + {"owner name is normalized", "de.telekom.foo.v1", "My_App", "de-telekom-foo-v1--my-app"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From d0c6eb39c8268344e77484a79d27adcd7844e881 Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Mon, 27 Jul 2026 17:13:44 +0300 Subject: [PATCH 17/31] backmerge from main --- file/api/go.mod | 32 +++++++-------- file/api/go.sum | 15 +++++++ rover/cmd/main.go | 5 --- rover/go.mod | 8 +--- rover/go.sum | 4 +- rover/internal/handler/rover/handler.go | 54 +++++++++++-------------- 6 files changed, 59 insertions(+), 59 deletions(-) diff --git a/file/api/go.mod b/file/api/go.mod index 26d52e322..48fc2697a 100644 --- a/file/api/go.mod +++ b/file/api/go.mod @@ -4,15 +4,15 @@ module github.com/telekom/controlplane/file/api -go 1.26.4 +go 1.26.5 require ( - github.com/onsi/ginkgo/v2 v2.28.3 - github.com/onsi/gomega v1.40.0 + github.com/onsi/ginkgo/v2 v2.32.0 + github.com/onsi/gomega v1.42.1 github.com/telekom/controlplane/common v0.0.0 - k8s.io/apiextensions-apiserver v0.36.0 - k8s.io/apimachinery v0.36.0 - sigs.k8s.io/controller-runtime v0.24.0 + k8s.io/apiextensions-apiserver v0.36.2 + k8s.io/apimachinery v0.36.2 + sigs.k8s.io/controller-runtime v0.24.1 ) require ( @@ -46,22 +46,22 @@ require ( github.com/spf13/viper v1.21.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.36.0 // indirect - golang.org/x/net v0.55.0 // indirect + golang.org/x/mod v0.38.0 // indirect + golang.org/x/net v0.56.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/term v0.43.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.38.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/tools v0.47.0 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.36.0 // indirect - k8s.io/client-go v0.36.0 // indirect + k8s.io/api v0.36.2 // indirect + k8s.io/client-go v0.36.2 // indirect k8s.io/klog/v2 v2.140.0 // indirect k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect diff --git a/file/api/go.sum b/file/api/go.sum index dca3badb6..18d634fbd 100644 --- a/file/api/go.sum +++ b/file/api/go.sum @@ -81,8 +81,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.28.3 h1:4JvMdwtFU0imd8fHx25OJXoDMRexnf8v5NHKYSTTji4= github.com/onsi/ginkgo/v2 v2.28.3/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/ginkgo/v2 v2.32.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc= github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= +github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -137,26 +139,34 @@ go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -171,12 +181,16 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80= k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34= +k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg= k8s.io/apiextensions-apiserver v0.36.0 h1:Wt7E8J+VBCbj4FjiBfDTK/neXDDjyJVJc7xfuOHImZ0= k8s.io/apiextensions-apiserver v0.36.0/go.mod h1:kGDjH0msuiIB3tgsYRV0kS9GqpMYMUsQ3GHv7TApyug= +k8s.io/apiextensions-apiserver v0.36.2/go.mod h1:cL1tBWe8XSaP1H30iWKGo7hf6iAUUUJPEU70dskmAnA= k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ= k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc= +k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4= k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c= k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y= +k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= @@ -185,6 +199,7 @@ k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0x k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= sigs.k8s.io/controller-runtime v0.24.0 h1:Ck6N2LdS8Lovy1o25BB4r1xjvLEKUl1s2o9kU+KWDE4= sigs.k8s.io/controller-runtime v0.24.0/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= +sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= diff --git a/rover/cmd/main.go b/rover/cmd/main.go index cbe242423..c7c87244a 100644 --- a/rover/cmd/main.go +++ b/rover/cmd/main.go @@ -11,7 +11,6 @@ import ( "os" filev1 "github.com/telekom/controlplane/file/api/v1" - "sigs.k8s.io/controller-runtime/pkg/metrics" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. @@ -42,10 +41,6 @@ import ( webhookv1 "github.com/telekom/controlplane/rover/internal/webhook/v1" secretsapi "github.com/telekom/controlplane/secret-manager/api" secretmetrics "github.com/telekom/controlplane/secret-manager/api/metrics" - - // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) - // to ensure that exec-entrypoint and run can make use of them. - _ "k8s.io/client-go/plugin/pkg/client/auth" ) var ( diff --git a/rover/go.mod b/rover/go.mod index 1b7046952..0af78cfe1 100644 --- a/rover/go.mod +++ b/rover/go.mod @@ -25,13 +25,9 @@ require ( github.com/onsi/gomega v1.42.1 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.11.1 - github.com/telekom/controlplane/file/api v0.0.0-00010101000000-000000000000 - golang.org/x/crypto v0.52.0 - k8s.io/api v0.36.0 - k8s.io/apimachinery v0.36.0 - k8s.io/client-go v0.36.0 - sigs.k8s.io/controller-runtime v0.24.0 github.com/telekom/controlplane/agentic/api v0.0.0-00010101000000-000000000000 + github.com/telekom/controlplane/file/api v0.0.0-00010101000000-000000000000 + golang.org/x/crypto v0.53.0 k8s.io/api v0.36.2 k8s.io/apimachinery v0.36.2 k8s.io/client-go v0.36.2 diff --git a/rover/go.sum b/rover/go.sum index de7674111..613e1fcf9 100644 --- a/rover/go.sum +++ b/rover/go.sum @@ -209,8 +209,8 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= diff --git a/rover/internal/handler/rover/handler.go b/rover/internal/handler/rover/handler.go index 453a82c3d..de0c551b6 100644 --- a/rover/internal/handler/rover/handler.go +++ b/rover/internal/handler/rover/handler.go @@ -82,11 +82,6 @@ func addKnownTypes(c client.JanitorClient) { c.AddKnownTypeToState(&filev1.FileExposure{}) c.AddKnownTypeToState(&filev1.FileSubscription{}) } - - // Create Application from Rover - err := application.HandleApplication(ctx, c, roverObj) - if err != nil { - return errors.Wrap(err, "failed to handle application") if config.FeatureAiGateway.IsEnabled() { c.AddKnownTypeToState(&agenticv1.McpExposure{}) c.AddKnownTypeToState(&agenticv1.McpSubscription{}) @@ -98,7 +93,6 @@ func (h *RoverHandler) handleExposures(ctx context.Context, c client.JanitorClie roverObj.Status.EventExposures = make([]types.ObjectRef, 0, len(roverObj.Spec.Exposures)) roverObj.Status.AiExposures = make([]types.ObjectRef, 0, len(roverObj.Spec.Exposures)) roverObj.Status.FileExposures = make([]types.ObjectRef, 0, len(roverObj.Spec.Exposures)) - seenDescriminators := make(map[string]struct{}) seenDiscriminators := make(map[string]struct{}) for _, exp := range roverObj.Spec.Exposures { @@ -142,18 +136,18 @@ func (h *RoverHandler) handleExposure(ctx context.Context, c client.JanitorClien return errors.Wrap(err, "failed to handle AI exposure") } case roverv1.TypeFile: - // Duplicate file types are rejected by the Rover admission webhook - if !config.FeatureFile.IsEnabled() { - log.Info("file exposure skipped, feature has not been enabled") - continue - } - if err := file.HandleExposure(ctx, c, roverObj, exp.File); err != nil { - return errors.Wrap(err, "failed to handle file exposure") - } - - default: - return errors.New("unknown exposure type: " + exp.Type().String()) + // Duplicate file types are rejected by the Rover admission webhook + if !config.FeatureFile.IsEnabled() { + logger.Info("file exposure skipped, feature has not been enabled") + return nil } + if err := file.HandleExposure(ctx, c, roverObj, exp.File); err != nil { + return errors.Wrap(err, "failed to handle file exposure") + } + + default: + return errors.New("unknown exposure type: " + exp.Type().String()) + } return nil } @@ -193,20 +187,20 @@ func (h *RoverHandler) handleSubscription(ctx context.Context, c client.JanitorC } if err := ai.HandleSubscription(ctx, c, roverObj, sub.Ai); err != nil { return errors.Wrap(err, "failed to handle AI subscription") - } - - case roverv1.TypeFile: - if !config.FeatureFile.IsEnabled() { - log.Info("file subscription skipped, feature has not been enabled") - continue - } - if err := file.HandleSubscription(ctx, c, roverObj, sub.File); err != nil { - return errors.Wrap(err, "failed to handle file subscription") - } - - default: - return errors.New("unknown subscription type: " + sub.Type().String()) } + + case roverv1.TypeFile: + if !config.FeatureFile.IsEnabled() { + logger.Info("file subscription skipped, feature has not been enabled") + return nil + } + if err := file.HandleSubscription(ctx, c, roverObj, sub.File); err != nil { + return errors.Wrap(err, "failed to handle file subscription") + } + + default: + return errors.New("unknown subscription type: " + sub.Type().String()) + } return nil } From 56d19fb29dd7f74eb038fab9ab46b41dc873c05b Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Mon, 3 Aug 2026 17:00:16 +0300 Subject: [PATCH 18/31] fix the conditional block --- rover-ctl/pkg/handlers/v0/rover.go | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/rover-ctl/pkg/handlers/v0/rover.go b/rover-ctl/pkg/handlers/v0/rover.go index 78a18f1cf..527abaa5c 100644 --- a/rover-ctl/pkg/handlers/v0/rover.go +++ b/rover-ctl/pkg/handlers/v0/rover.go @@ -121,15 +121,10 @@ func PatchExposures(exposures []any) []map[string]any { exposuresMaps[i]["type"] = "api" } else if _, exist := exposure["eventType"]; exist { exposuresMaps[i]["type"] = "event" - } + } else if _, exist := exposure["fileType"]; exist { + exposuresMaps[i]["type"] = "file" + } // TODO: add more types as needed } - if _, exist := exposure["basePath"]; exist { - exposuresMaps[i]["type"] = "api" - } else if _, exist := exposure["eventType"]; exist { - exposuresMaps[i]["type"] = "event" - } else if _, exist := exposure["fileType"]; exist { - exposuresMaps[i]["type"] = "file" - } // TODO: add more types as needed security, exist := exposure["security"] if exist { PatchSecurity(security) @@ -157,15 +152,10 @@ func PatchSubscriptions(subscriptions []any) []map[string]any { subscriptionsMaps[i]["type"] = "api" } else if _, exist := subscription["eventType"]; exist { subscriptionsMaps[i]["type"] = "event" - } + } else if _, exist := subscription["fileType"]; exist { + subscriptionsMaps[i]["type"] = "file" + } // TODO: add more types as needed } - if _, exist := subscription["basePath"]; exist { - subscriptionsMaps[i]["type"] = "api" - } else if _, exist := subscription["eventType"]; exist { - subscriptionsMaps[i]["type"] = "event" - } else if _, exist := subscription["fileType"]; exist { - subscriptionsMaps[i]["type"] = "file" - } // TODO: add more types as needed security, exist := subscription["security"] if exist { PatchSecurity(security) From 1e098e6ba28ed15754d43967ba7e2b7cfad2f47f Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Mon, 3 Aug 2026 17:59:52 +0300 Subject: [PATCH 19/31] add tests for coverage fix some issues --- .../internal/mapper/rover/in/file_test.go | 30 ++++++++++ rover/cmd/main.go | 2 +- .../filespecification_controller.go | 10 ++-- .../handler/filespecification/handler.go | 5 +- .../rover/application/application_test.go | 5 +- rover/internal/handler/rover/file/exposure.go | 11 ++-- .../handler/rover/file/subscription.go | 11 ++-- rover/internal/handler/rover/file/util.go | 3 +- .../internal/handler/rover/file/util_test.go | 6 ++ .../webhook/v1/filespecification_webhook.go | 3 +- .../v1/filespecification_webhook_test.go | 55 ++++++++++++++++++- rover/internal/webhook/v1/rover_webhook.go | 5 +- 12 files changed, 117 insertions(+), 29 deletions(-) diff --git a/rover-server/internal/mapper/rover/in/file_test.go b/rover-server/internal/mapper/rover/in/file_test.go index 02540a36e..3061ba6c2 100644 --- a/rover-server/internal/mapper/rover/in/file_test.go +++ b/rover-server/internal/mapper/rover/in/file_test.go @@ -51,6 +51,36 @@ var _ = Describe("File Type (SFTP) Mapper", func() { Expect(output.Visibility).To(BeEmpty()) }) + + It("must map ZONE visibility to the CRD Zone visibility", func() { + input := api.FileExposure{ + Type: "file", + FileType: "demo-sftp-spec-v1", + Visibility: api.ZONE, + PublicKeys: []api.PublicKey{ + {Label: "provider-key", Key: "ssh-ed25519 AAAA1"}, + }, + } + + output := mapFileExposure(input) + + Expect(output.Visibility).To(Equal(roverv1.VisibilityZone)) + }) + + It("must map ENTERPRISE visibility to the CRD Enterprise visibility", func() { + input := api.FileExposure{ + Type: "file", + FileType: "demo-sftp-spec-v1", + Visibility: api.ENTERPRISE, + PublicKeys: []api.PublicKey{ + {Label: "provider-key", Key: "ssh-ed25519 AAAA1"}, + }, + } + + output := mapFileExposure(input) + + Expect(output.Visibility).To(Equal(roverv1.VisibilityEnterprise)) + }) }) Context("mapFileSubscription", func() { diff --git a/rover/cmd/main.go b/rover/cmd/main.go index c7c87244a..8e900c780 100644 --- a/rover/cmd/main.go +++ b/rover/cmd/main.go @@ -71,7 +71,7 @@ func init() { if cconfig.FeatureFile.IsEnabled() { utilruntime.Must(filev1.AddToScheme(scheme)) } - //+kubebuilder:scaffold:scheme + // +kubebuilder:scaffold:scheme } func main() { diff --git a/rover/internal/controller/filespecification_controller.go b/rover/internal/controller/filespecification_controller.go index 0cc9052cc..62d612b2e 100644 --- a/rover/internal/controller/filespecification_controller.go +++ b/rover/internal/controller/filespecification_controller.go @@ -2,23 +2,23 @@ // // SPDX-License-Identifier: Apache-2.0 +//nolint:dupl // Single-resource controller scaffolds are intentionally kept parallel for clarity. package controller import ( "context" - cconfig "github.com/telekom/controlplane/common/pkg/config" - cc "github.com/telekom/controlplane/common/pkg/controller" - filev1 "github.com/telekom/controlplane/file/api/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" - filespec_handler "github.com/telekom/controlplane/rover/internal/handler/filespecification" - + cconfig "github.com/telekom/controlplane/common/pkg/config" + cc "github.com/telekom/controlplane/common/pkg/controller" + filev1 "github.com/telekom/controlplane/file/api/v1" rover "github.com/telekom/controlplane/rover/api/v1" + filespec_handler "github.com/telekom/controlplane/rover/internal/handler/filespecification" ) // FileSpecificationReconciler reconciles a FileSpecification object diff --git a/rover/internal/handler/filespecification/handler.go b/rover/internal/handler/filespecification/handler.go index c98314830..9abbc7596 100644 --- a/rover/internal/handler/filespecification/handler.go +++ b/rover/internal/handler/filespecification/handler.go @@ -8,6 +8,9 @@ import ( "context" "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "github.com/telekom/controlplane/common/pkg/client" "github.com/telekom/controlplane/common/pkg/condition" "github.com/telekom/controlplane/common/pkg/handler" @@ -15,8 +18,6 @@ import ( "github.com/telekom/controlplane/common/pkg/util/labelutil" filev1 "github.com/telekom/controlplane/file/api/v1" roverv1 "github.com/telekom/controlplane/rover/api/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) var _ handler.Handler[*roverv1.FileSpecification] = (*FileSpecificationHandler)(nil) diff --git a/rover/internal/handler/rover/application/application_test.go b/rover/internal/handler/rover/application/application_test.go index 9e7b9b507..121e7cab8 100644 --- a/rover/internal/handler/rover/application/application_test.go +++ b/rover/internal/handler/rover/application/application_test.go @@ -7,11 +7,12 @@ package application import ( "testing" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" roverv1 "github.com/telekom/controlplane/rover/api/v1" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) func TestApplication(t *testing.T) { diff --git a/rover/internal/handler/rover/file/exposure.go b/rover/internal/handler/rover/file/exposure.go index b8c3bc6e8..58cf9e800 100644 --- a/rover/internal/handler/rover/file/exposure.go +++ b/rover/internal/handler/rover/file/exposure.go @@ -8,6 +8,10 @@ import ( "context" "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/telekom/controlplane/common/pkg/client" "github.com/telekom/controlplane/common/pkg/config" "github.com/telekom/controlplane/common/pkg/types" @@ -15,15 +19,12 @@ import ( "github.com/telekom/controlplane/common/pkg/util/labelutil" filev1 "github.com/telekom/controlplane/file/api/v1" roverv1 "github.com/telekom/controlplane/rover/api/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/log" ) // HandleExposure creates or updates a file-domain FileExposure owned by the Rover. func HandleExposure(ctx context.Context, c client.JanitorClient, owner *roverv1.Rover, exp *roverv1.FileExposure) error { - log := log.FromContext(ctx) - log.V(1).Info("Handle FileExposure", "fileType", exp.FileType) + logger := log.FromContext(ctx) + logger.V(1).Info("Handle FileExposure", "fileType", exp.FileType) name := MakeName(exp.FileType, owner.Name) diff --git a/rover/internal/handler/rover/file/subscription.go b/rover/internal/handler/rover/file/subscription.go index a3e87df5c..ff5ab431f 100644 --- a/rover/internal/handler/rover/file/subscription.go +++ b/rover/internal/handler/rover/file/subscription.go @@ -8,21 +8,22 @@ import ( "context" "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/telekom/controlplane/common/pkg/client" "github.com/telekom/controlplane/common/pkg/config" "github.com/telekom/controlplane/common/pkg/types" "github.com/telekom/controlplane/common/pkg/util/labelutil" filev1 "github.com/telekom/controlplane/file/api/v1" roverv1 "github.com/telekom/controlplane/rover/api/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/log" ) // HandleSubscription creates or updates a file-domain FileSubscription owned by the Rover. func HandleSubscription(ctx context.Context, c client.JanitorClient, owner *roverv1.Rover, sub *roverv1.FileSubscription) error { - log := log.FromContext(ctx) - log.V(1).Info("Handle FileSubscription", "fileType", sub.FileType) + logger := log.FromContext(ctx) + logger.V(1).Info("Handle FileSubscription", "fileType", sub.FileType) name := MakeName(sub.FileType, owner.Name) diff --git a/rover/internal/handler/rover/file/util.go b/rover/internal/handler/rover/file/util.go index 78bc9361e..59ff060c0 100644 --- a/rover/internal/handler/rover/file/util.go +++ b/rover/internal/handler/rover/file/util.go @@ -5,10 +5,9 @@ package file import ( + "github.com/telekom/controlplane/common/pkg/util/labelutil" filev1 "github.com/telekom/controlplane/file/api/v1" roverv1 "github.com/telekom/controlplane/rover/api/v1" - - "github.com/telekom/controlplane/common/pkg/util/labelutil" ) // MakeName generates a deterministic resource name for a file exposure or diff --git a/rover/internal/handler/rover/file/util_test.go b/rover/internal/handler/rover/file/util_test.go index 888184017..9a8c52bb4 100644 --- a/rover/internal/handler/rover/file/util_test.go +++ b/rover/internal/handler/rover/file/util_test.go @@ -38,6 +38,12 @@ func TestMapPublicKeys(t *testing.T) { } }) + t.Run("empty slice yields nil", func(t *testing.T) { + if got := mapPublicKeys([]roverv1.PublicKey{}); got != nil { + t.Errorf("mapPublicKeys([]) = %v, want nil", got) + } + }) + t.Run("maps label and key preserving order", func(t *testing.T) { in := []roverv1.PublicKey{ {Label: "provider-key", Key: "ssh-ed25519 AAAA"}, diff --git a/rover/internal/webhook/v1/filespecification_webhook.go b/rover/internal/webhook/v1/filespecification_webhook.go index 5fde27141..e24aabd55 100644 --- a/rover/internal/webhook/v1/filespecification_webhook.go +++ b/rover/internal/webhook/v1/filespecification_webhook.go @@ -39,7 +39,7 @@ func (v *FileSpecificationCustomValidator) ValidateCreate(ctx context.Context, f } // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type FileSpecification. -func (v *FileSpecificationCustomValidator) ValidateUpdate(ctx context.Context, _ *roverv1.FileSpecification, filespecification *roverv1.FileSpecification) (admission.Warnings, error) { +func (v *FileSpecificationCustomValidator) ValidateUpdate(ctx context.Context, _, filespecification *roverv1.FileSpecification) (admission.Warnings, error) { return v.ValidateCreateOrUpdate(ctx, filespecification) } @@ -49,7 +49,6 @@ func (v *FileSpecificationCustomValidator) ValidateDelete(ctx context.Context, f } func (v *FileSpecificationCustomValidator) ValidateCreateOrUpdate(ctx context.Context, filespecification *roverv1.FileSpecification) (admission.Warnings, error) { - if controller.IsBeingDeleted(filespecification) { return nil, nil } diff --git a/rover/internal/webhook/v1/filespecification_webhook_test.go b/rover/internal/webhook/v1/filespecification_webhook_test.go index 260543ac9..0351a7147 100644 --- a/rover/internal/webhook/v1/filespecification_webhook_test.go +++ b/rover/internal/webhook/v1/filespecification_webhook_test.go @@ -13,18 +13,18 @@ import ( "crypto/rsa" "strings" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" "golang.org/x/crypto/ssh" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" cerrors "github.com/telekom/controlplane/common/pkg/errors" roverv1 "github.com/telekom/controlplane/rover/api/v1" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) var _ = Describe("File Type (SFTP) Validation", func() { - newValErr := func() *cerrors.ValidationError { return cerrors.NewValidationError(roverv1.GroupVersion.WithKind("Rover").GroupKind(), NewRover(testZone)) } @@ -162,6 +162,55 @@ var _ = Describe("File Type (SFTP) Validation", func() { }) }) + Context("file validation via ValidateCreate (Rover webhook dispatch)", func() { + var validator RoverValidator + + BeforeEach(func() { + validator = RoverValidator{client: k8sClient} + }) + + validKey := func(label string) roverv1.PublicKey { + return roverv1.PublicKey{Label: label, Key: newED25519Key()} + } + + It("should reject a file exposure that has no public keys", func() { + rover := NewRover(testZone) + rover.Spec.Exposures = []roverv1.Exposure{ + {File: &roverv1.FileExposure{FileType: "demo-sftp-spec-v1"}}, + } + warnings, err := validator.ValidateCreate(ctx, rover) + assertValidationFailedWith(warnings, err, "at least one public key must be specified") + }) + + It("should reject a file subscription that has no public keys", func() { + rover := NewRover(testZone) + rover.Spec.Subscriptions = []roverv1.Subscription{ + {File: &roverv1.FileSubscription{FileType: "demo-sftp-spec-v1"}}, + } + warnings, err := validator.ValidateCreate(ctx, rover) + assertValidationFailedWith(warnings, err, "at least one public key must be specified") + }) + + It("should accept a file exposure and subscription that share the same fileType", func() { + rover := NewRover(testZone) + rover.Spec.Exposures = []roverv1.Exposure{ + {File: &roverv1.FileExposure{ + FileType: "demo-sftp-spec-v1", + PublicKeys: []roverv1.PublicKey{validKey("provider-key")}, + }}, + } + rover.Spec.Subscriptions = []roverv1.Subscription{ + {File: &roverv1.FileSubscription{ + FileType: "demo-sftp-spec-v1", + PublicKeys: []roverv1.PublicKey{validKey("consumer-key")}, + }}, + } + warnings, err := validator.ValidateCreate(ctx, rover) + Expect(warnings).To(BeNil()) + Expect(err).NotTo(HaveOccurred()) + }) + }) + Context("FileSpecificationCustomValidator", func() { var validator *FileSpecificationCustomValidator diff --git a/rover/internal/webhook/v1/rover_webhook.go b/rover/internal/webhook/v1/rover_webhook.go index b64ef3c2e..5615ee8b9 100644 --- a/rover/internal/webhook/v1/rover_webhook.go +++ b/rover/internal/webhook/v1/rover_webhook.go @@ -14,6 +14,7 @@ import ( "github.com/go-logr/logr" "github.com/pkg/errors" + "golang.org/x/crypto/ssh" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" @@ -29,8 +30,6 @@ import ( eventv1 "github.com/telekom/controlplane/event/api/v1" organizationv1 "github.com/telekom/controlplane/organization/api/v1" roverv1 "github.com/telekom/controlplane/rover/api/v1" - "golang.org/x/crypto/ssh" - secretsapi "github.com/telekom/controlplane/secret-manager/api" ) @@ -427,6 +426,8 @@ func CheckWeightSetOnAllOrNone(upstreams []roverv1.Upstream) (allSet, noneSet bo } // MustNotHaveDuplicates checks if there are no duplicates in the subscriptions and exposures +// +//nolint:dupl // subscription and exposure loops mirror each other but operate on different types func MustNotHaveDuplicates(valErr *cerrors.ValidationError, subs []roverv1.Subscription, exps []roverv1.Exposure) error { if len(subs) == 0 && len(exps) == 0 { return nil // No subscriptions or exposures, no duplicates to check From 661c1a8da70efcc125ea5299b5f967b2f8158ea6 Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Tue, 4 Aug 2026 14:32:03 +0300 Subject: [PATCH 20/31] add tests --- file/api/go.mod | 2 +- file/api/go.sum | 1 + .../internal/mapper/rover/out/file_test.go | 92 ++++++++++++++++ rover/api/v1/filespecification_types_test.go | 104 ++++++++++++++++++ 4 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 rover-server/internal/mapper/rover/out/file_test.go create mode 100644 rover/api/v1/filespecification_types_test.go diff --git a/file/api/go.mod b/file/api/go.mod index 48fc2697a..231177c0a 100644 --- a/file/api/go.mod +++ b/file/api/go.mod @@ -54,7 +54,7 @@ require ( golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/term v0.44.0 // indirect - golang.org/x/text v0.38.0 // indirect + golang.org/x/text v0.39.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.47.0 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect diff --git a/file/api/go.sum b/file/api/go.sum index 18d634fbd..68bec845a 100644 --- a/file/api/go.sum +++ b/file/api/go.sum @@ -162,6 +162,7 @@ golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= diff --git a/rover-server/internal/mapper/rover/out/file_test.go b/rover-server/internal/mapper/rover/out/file_test.go new file mode 100644 index 000000000..c7fb4a6f0 --- /dev/null +++ b/rover-server/internal/mapper/rover/out/file_test.go @@ -0,0 +1,92 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package out + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + roverv1 "github.com/telekom/controlplane/rover/api/v1" + + "github.com/telekom/controlplane/rover-server/internal/api" +) + +var _ = Describe("File Type (SFTP) Exposure Mapper", func() { + + Context("mapFileExposure", func() { + It("must map a FileExposure correctly", func() { + input := &roverv1.FileExposure{ + FileType: "demo-sftp-spec-v1", + Visibility: roverv1.VisibilityWorld, + PublicKeys: []roverv1.PublicKey{ + {Label: "provider-key", Key: "ssh-ed25519 AAAA1"}, + }, + } + + output := mapFileExposure(input) + + Expect(output.FileType).To(Equal("demo-sftp-spec-v1")) + Expect(output.Visibility).To(Equal(api.WORLD)) + Expect(output.PublicKeys).To(HaveLen(1)) + Expect(output.PublicKeys[0].Label).To(Equal("provider-key")) + Expect(output.PublicKeys[0].Key).To(Equal("ssh-ed25519 AAAA1")) + }) + + DescribeTable("must map visibility to the API visibility", + func(in roverv1.Visibility, expected api.Visibility) { + output := mapFileExposure(&roverv1.FileExposure{ + FileType: "demo-sftp-spec-v1", + Visibility: in, + PublicKeys: []roverv1.PublicKey{{Label: "provider-key", Key: "ssh-ed25519 AAAA1"}}, + }) + Expect(output.Visibility).To(Equal(expected)) + }, + Entry("WORLD", roverv1.VisibilityWorld, api.WORLD), + Entry("ZONE", roverv1.VisibilityZone, api.ZONE), + Entry("ENTERPRISE", roverv1.VisibilityEnterprise, api.ENTERPRISE), + ) + }) + + Context("mapPublicKeys", func() { + It("must return nil for an empty list", func() { + Expect(mapPublicKeys(nil)).To(BeNil()) + Expect(mapPublicKeys([]roverv1.PublicKey{})).To(BeNil()) + }) + + It("must preserve order and values", func() { + output := mapPublicKeys([]roverv1.PublicKey{ + {Label: "a", Key: "k1"}, + {Label: "b", Key: "k2"}, + }) + + Expect(output).To(HaveLen(2)) + Expect(output[0]).To(Equal(api.PublicKey{Label: "a", Key: "k1"})) + Expect(output[1]).To(Equal(api.PublicKey{Label: "b", Key: "k2"})) + }) + }) + + Context("mapExposure dispatch", func() { + It("must map a FileExposure via the discriminator", func() { + input := &roverv1.Exposure{ + File: &roverv1.FileExposure{ + FileType: "demo-sftp-spec-v1", + Visibility: roverv1.VisibilityWorld, + PublicKeys: []roverv1.PublicKey{ + {Label: "provider-key", Key: "ssh-ed25519 AAAA1"}, + }, + }, + } + output := &api.Exposure{} + + err := mapExposure(input, output) + Expect(err).To(BeNil()) + + fileExposure, err := output.AsFileExposure() + Expect(err).To(BeNil()) + Expect(fileExposure.FileType).To(Equal("demo-sftp-spec-v1")) + Expect(fileExposure.Visibility).To(Equal(api.WORLD)) + Expect(fileExposure.PublicKeys).To(HaveLen(1)) + }) + }) +}) diff --git a/rover/api/v1/filespecification_types_test.go b/rover/api/v1/filespecification_types_test.go new file mode 100644 index 000000000..bf4a47c38 --- /dev/null +++ b/rover/api/v1/filespecification_types_test.go @@ -0,0 +1,104 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package v1_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "github.com/telekom/controlplane/rover/api/v1" +) + +var _ = Describe("FileSpecification Types", func() { + Context("MakeFileSpecificationName", func() { + DescribeTable("normalizes the FileSpecification name", + func(name, expected string) { + fileSpec := &v1.FileSpecification{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + } + Expect(v1.MakeFileSpecificationName(fileSpec)).To(Equal(expected)) + }, + Entry("dotted name is hyphenated", "de.telekom.foo.v1", "de-telekom-foo-v1"), + Entry("mixed case is lowercased", "De.Telekom.V1", "de-telekom-v1"), + Entry("already hyphenated name is unchanged", "demo-sftp-spec-v1", "demo-sftp-spec-v1"), + ) + }) + + Context("FileStorageType", func() { + It("should stringify the sftp storage type", func() { + Expect(v1.FileStorageTypeSFTP.String()).To(Equal("sftp")) + }) + }) + + Context("FileSpecification conditions", func() { + It("should set and get conditions", func() { + fileSpec := &v1.FileSpecification{} + Expect(fileSpec.GetConditions()).To(BeEmpty()) + + changed := fileSpec.SetCondition(metav1.Condition{ + Type: "Ready", + Status: metav1.ConditionTrue, + Reason: "Provisioned", + Message: "FileType is ready", + }) + Expect(changed).To(BeTrue()) + + conditions := fileSpec.GetConditions() + Expect(conditions).To(HaveLen(1)) + Expect(conditions[0].Type).To(Equal("Ready")) + Expect(conditions[0].Status).To(Equal(metav1.ConditionTrue)) + + // Setting the same condition again reports no change. + changed = fileSpec.SetCondition(metav1.Condition{ + Type: "Ready", + Status: metav1.ConditionTrue, + Reason: "Provisioned", + Message: "FileType is ready", + }) + Expect(changed).To(BeFalse()) + }) + }) + + Context("FileSpecificationList", func() { + It("should return its items as types.Object", func() { + list := &v1.FileSpecificationList{ + Items: []v1.FileSpecification{ + {ObjectMeta: metav1.ObjectMeta{Name: "spec-a"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "spec-b"}}, + }, + } + + items := list.GetItems() + Expect(items).To(HaveLen(2)) + Expect(items[0].GetName()).To(Equal("spec-a")) + Expect(items[1].GetName()).To(Equal("spec-b")) + }) + + It("should return an empty slice for an empty list", func() { + list := &v1.FileSpecificationList{} + Expect(list.GetItems()).To(BeEmpty()) + }) + }) + + Context("SSHKeyType", func() { + It("should stringify the supported key types", func() { + Expect(v1.SSHKeyTypeRSA.String()).To(Equal("ssh-rsa")) + Expect(v1.SSHKeyTypeECDSANistP521.String()).To(Equal("ecdsa-sha2-nistp521")) + Expect(v1.SSHKeyTypeED25519.String()).To(Equal("ssh-ed25519")) + }) + + DescribeTable("reports validity of a key type", + func(keyType v1.SSHKeyType, valid bool) { + Expect(keyType.IsValid()).To(Equal(valid)) + }, + Entry("ssh-rsa is valid", v1.SSHKeyTypeRSA, true), + Entry("ecdsa-sha2-nistp521 is valid", v1.SSHKeyTypeECDSANistP521, true), + Entry("ssh-ed25519 is valid", v1.SSHKeyTypeED25519, true), + Entry("unsupported ecdsa-sha2-nistp256 is invalid", v1.SSHKeyType("ecdsa-sha2-nistp256"), false), + Entry("empty is invalid", v1.SSHKeyType(""), false), + ) + }) +}) From d02a25ffeb72176ff1ee666eca0ab76616047e7d Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Tue, 4 Aug 2026 16:22:02 +0300 Subject: [PATCH 21/31] add tests --- .../rover/in/__snapshots__/exposure_test.snap | 13 ++-- .../rover/in/__snapshots__/rover_test.snap | 20 ++--- .../in/__snapshots__/subscription_test.snap | 12 +-- rover/api/v1/zz_generated.deepcopy.go | 40 +++++----- ...r.cp.ei.telekom.de_filespecifications.yaml | 16 ++-- .../bases/rover.cp.ei.telekom.de_rovers.yaml | 73 +++++++++++++++---- rover/config/rbac/role.yaml | 6 +- 7 files changed, 113 insertions(+), 67 deletions(-) diff --git a/rover-server/internal/mapper/rover/in/__snapshots__/exposure_test.snap b/rover-server/internal/mapper/rover/in/__snapshots__/exposure_test.snap index 2cff58a0c..8480888b4 100755 --- a/rover-server/internal/mapper/rover/in/__snapshots__/exposure_test.snap +++ b/rover-server/internal/mapper/rover/in/__snapshots__/exposure_test.snap @@ -47,7 +47,7 @@ }, Event: (*v1.EventExposure)(nil), Agentic: (*v1.AgenticExposure)(nil), - File: (*v1.FileExposure)(nil), + File: (*v1.FileExposure)(nil), } --- @@ -65,7 +65,7 @@ AdditionalPublisherIds: nil, }, Agentic: (*v1.AgenticExposure)(nil), - File: (*v1.FileExposure)(nil), + File: (*v1.FileExposure)(nil), } --- @@ -75,12 +75,13 @@ [Exposure Mapper mapExposure must map a FileExposure correctly - 1] &v1.Exposure{ - Api: (*v1.ApiExposure)(nil), - Event: (*v1.EventExposure)(nil), - File: &v1.FileExposure{ + Api: (*v1.ApiExposure)(nil), + Event: (*v1.EventExposure)(nil), + Agentic: (*v1.AgenticExposure)(nil), + File: &v1.FileExposure{ FileType: "demo-sftp-spec-v1", - Variant: "sftp", Visibility: "World", + Approval: v1.Approval{}, PublicKeys: { {Label:"provider-key", Key:"ssh-ed25519 AAAA-provider"}, }, diff --git a/rover-server/internal/mapper/rover/in/__snapshots__/rover_test.snap b/rover-server/internal/mapper/rover/in/__snapshots__/rover_test.snap index 1b388f4f4..63e08e7dc 100644 --- a/rover-server/internal/mapper/rover/in/__snapshots__/rover_test.snap +++ b/rover-server/internal/mapper/rover/in/__snapshots__/rover_test.snap @@ -26,7 +26,7 @@ }, Event: (*v1.EventExposure)(nil), Agentic: (*v1.AgenticExposure)(nil), - File: (*v1.FileExposure)(nil), + File: (*v1.FileExposure)(nil), }, }, Subscriptions: { @@ -40,7 +40,7 @@ }, Event: (*v1.EventSubscription)(nil), Agentic: (*v1.AgenticSubscription)(nil), - File: (*v1.FileSubscription)(nil), + File: (*v1.FileSubscription)(nil), }, }, Permissions: nil, @@ -77,7 +77,7 @@ }, Event: (*v1.EventExposure)(nil), Agentic: (*v1.AgenticExposure)(nil), - File: (*v1.FileExposure)(nil), + File: (*v1.FileExposure)(nil), }, }, Subscriptions: nil, @@ -109,7 +109,7 @@ }, Event: (*v1.EventSubscription)(nil), Agentic: (*v1.AgenticSubscription)(nil), - File: (*v1.FileSubscription)(nil), + File: (*v1.FileSubscription)(nil), }, }, Permissions: nil, @@ -162,7 +162,7 @@ }, Event: (*v1.EventExposure)(nil), Agentic: (*v1.AgenticExposure)(nil), - File: (*v1.FileExposure)(nil), + File: (*v1.FileExposure)(nil), }, }, Subscriptions: { @@ -176,7 +176,7 @@ }, Event: (*v1.EventSubscription)(nil), Agentic: (*v1.AgenticSubscription)(nil), - File: (*v1.FileSubscription)(nil), + File: (*v1.FileSubscription)(nil), }, }, Permissions: nil, @@ -254,7 +254,7 @@ }, Event: (*v1.EventExposure)(nil), Agentic: (*v1.AgenticExposure)(nil), - File: (*v1.FileExposure)(nil), + File: (*v1.FileExposure)(nil), }, }, Subscriptions: { @@ -268,7 +268,7 @@ }, Event: (*v1.EventSubscription)(nil), Agentic: (*v1.AgenticSubscription)(nil), - File: (*v1.FileSubscription)(nil), + File: (*v1.FileSubscription)(nil), }, }, Permissions: nil, @@ -321,7 +321,7 @@ }, Event: (*v1.EventExposure)(nil), Agentic: (*v1.AgenticExposure)(nil), - File: (*v1.FileExposure)(nil), + File: (*v1.FileExposure)(nil), }, }, Subscriptions: { @@ -335,7 +335,7 @@ }, Event: (*v1.EventSubscription)(nil), Agentic: (*v1.AgenticSubscription)(nil), - File: (*v1.FileSubscription)(nil), + File: (*v1.FileSubscription)(nil), }, }, Permissions: nil, diff --git a/rover-server/internal/mapper/rover/in/__snapshots__/subscription_test.snap b/rover-server/internal/mapper/rover/in/__snapshots__/subscription_test.snap index 49b7851b8..0fdc2b128 100755 --- a/rover-server/internal/mapper/rover/in/__snapshots__/subscription_test.snap +++ b/rover-server/internal/mapper/rover/in/__snapshots__/subscription_test.snap @@ -24,7 +24,7 @@ }, Event: (*v1.EventSubscription)(nil), Agentic: (*v1.AgenticSubscription)(nil), - File: (*v1.FileSubscription)(nil), + File: (*v1.FileSubscription)(nil), } --- @@ -38,7 +38,7 @@ Scopes: nil, }, Agentic: (*v1.AgenticSubscription)(nil), - File: (*v1.FileSubscription)(nil), + File: (*v1.FileSubscription)(nil), } --- @@ -48,11 +48,11 @@ [Subscription Mapper mapSubscription must map a FileSubscription correctly - 1] &v1.Subscription{ - Api: (*v1.ApiSubscription)(nil), - Event: (*v1.EventSubscription)(nil), - File: &v1.FileSubscription{ + Api: (*v1.ApiSubscription)(nil), + Event: (*v1.EventSubscription)(nil), + Agentic: (*v1.AgenticSubscription)(nil), + File: &v1.FileSubscription{ FileType: "demo-sftp-spec-v1", - Variant: "sftp", PublicKeys: { {Label:"consumer-key", Key:"ssh-ed25519 AAAA-consumer"}, }, diff --git a/rover/api/v1/zz_generated.deepcopy.go b/rover/api/v1/zz_generated.deepcopy.go index fd8fe4739..bef303bd4 100644 --- a/rover/api/v1/zz_generated.deepcopy.go +++ b/rover/api/v1/zz_generated.deepcopy.go @@ -1291,6 +1291,21 @@ func (in *ProviderFailover) DeepCopy() *ProviderFailover { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PublicKey) DeepCopyInto(out *PublicKey) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicKey. +func (in *PublicKey) DeepCopy() *PublicKey { + if in == nil { + return nil + } + out := new(PublicKey) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RateLimit) DeepCopyInto(out *RateLimit) { *out = *in @@ -1625,21 +1640,6 @@ func (in *RoverSpec) DeepCopy() *RoverSpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PublicKey) DeepCopyInto(out *PublicKey) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicKey. -func (in *PublicKey) DeepCopy() *PublicKey { - if in == nil { - return nil - } - out := new(PublicKey) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RoverStatus) DeepCopyInto(out *RoverStatus) { *out = *in @@ -1847,16 +1847,16 @@ func (in *Subscription) DeepCopyInto(out *Subscription) { *out = new(EventSubscription) (*in).DeepCopyInto(*out) } - if in.File != nil { - in, out := &in.File, &out.File - *out = new(FileSubscription) - (*in).DeepCopyInto(*out) - } if in.Agentic != nil { in, out := &in.Agentic, &out.Agentic *out = new(AgenticSubscription) (*in).DeepCopyInto(*out) } + if in.File != nil { + in, out := &in.File, &out.File + *out = new(FileSubscription) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Subscription. diff --git a/rover/config/crd/bases/rover.cp.ei.telekom.de_filespecifications.yaml b/rover/config/crd/bases/rover.cp.ei.telekom.de_filespecifications.yaml index e43189330..2d78187c8 100644 --- a/rover/config/crd/bases/rover.cp.ei.telekom.de_filespecifications.yaml +++ b/rover/config/crd/bases/rover.cp.ei.telekom.de_filespecifications.yaml @@ -44,7 +44,10 @@ spec: metadata: type: object spec: - description: FileSpecificationSpec defines the desired state of FileSpecification. + description: |- + FileSpecificationSpec defines the desired state of FileSpecification. + It mirrors the internal Rover-domain form from spec_dcp: only description and the + backend selector are stored; the file type identifier lives in metadata.name. properties: description: description: Description provides a human-readable summary of this @@ -57,8 +60,7 @@ spec: type: string storageType: default: sftp - description: 'StorageType selects the file-transfer backend. Currently - only "sftp".' + description: StorageType selects the file-transfer backend. enum: - sftp type: string @@ -126,8 +128,11 @@ spec: - type x-kubernetes-list-type: map fileType: - description: FileType references the file-domain resource created from - this specification. + description: |- + FileType references the file-domain FileType created from this specification. + It is populated by the FileSpecification reconciler + (rover/internal/controller/filespecification_controller.go), mirroring how + ApiSpecification creates Api and EventSpecification creates EventType. properties: name: type: string @@ -149,4 +154,3 @@ spec: storage: true subresources: status: {} - diff --git a/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml b/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml index 1389064e3..0aecc239b 100644 --- a/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml +++ b/rover/config/crd/bases/rover.cp.ei.telekom.de_rovers.yaml @@ -1117,6 +1117,47 @@ spec: description: File defines a File-based (SFTP) service exposure configuration properties: + approval: + description: Approval defines the approval workflow required + for subscriptions to this file type + properties: + strategy: + default: Simple + description: Strategy defines the approval process required + for this API + enum: + - Auto + - Simple + - FourEyes + type: string + trustedTeams: + description: |- + TrustedTeams identifies teams that are trusted for approving this API + Per default your own team is trusted + items: + description: TrustedTeam identifies a team that is + trusted for approvals + properties: + group: + description: Group identifies the organizational + group for this trusted team + minLength: 1 + type: string + team: + description: Team identifies the specific team + within the group + minLength: 1 + type: string + required: + - group + - team + type: object + maxItems: 10 + minItems: 0 + type: array + required: + - strategy + type: object fileType: description: |- FileType identifies the file type that is exposed. It must match the @@ -1137,8 +1178,8 @@ spec: minLength: 1 type: string label: - description: Label is a human-readable identifier for - the key. It must be unique per fileType. + description: Label is a human-readable identifier + for the key. It must be unique per fileType. minLength: 1 type: string required: @@ -1147,13 +1188,6 @@ spec: type: object minItems: 1 type: array - variant: - description: |- - Variant selects the file-transfer backend. Currently only "sftp" is - supported. - enum: - - sftp - type: string visibility: default: Enterprise description: Visibility defines who can see and subscribe @@ -1164,6 +1198,7 @@ spec: - Enterprise type: string required: + - approval - fileType - publicKeys - visibility @@ -1714,7 +1749,7 @@ spec: fileType: description: |- FileType identifies the file type to consume. It must match the - name (and spec.type) of an applied FileSpecification. + name of an applied FileSpecification. minLength: 1 type: string publicKeys: @@ -1731,8 +1766,8 @@ spec: minLength: 1 type: string label: - description: Label is a human-readable identifier for - the key. It must be unique per fileType. + description: Label is a human-readable identifier + for the key. It must be unique per fileType. minLength: 1 type: string required: @@ -1976,8 +2011,12 @@ spec: type: object type: array fileExposures: - description: FileExposures are references to FileExposure resources - created by this Rover in the file domain + description: |- + FileExposures are references to FileExposure resources created by this Rover in the file domain. + + (make(..., 0)); it is populated (append of the created file-domain resource refs) + by the file handler dispatch once the file domain module is available. + Populated from: rover/internal/handler/rover/handler.go, case roverv1.TypeFile. items: description: |- ObjectRef is a reference to a Kubernetes object @@ -1999,8 +2038,10 @@ spec: type: object type: array fileSubscriptions: - description: FileSubscriptions are references to FileSubscription resources - created by this Rover in the file domain + description: |- + FileSubscriptions are references to FileSubscription resources created by this Rover in the file domain. + + (rover/internal/handler/rover/handler.go, case roverv1.TypeFile) once delivered. items: description: |- ObjectRef is a reference to a Kubernetes object diff --git a/rover/config/rbac/role.yaml b/rover/config/rbac/role.yaml index 49d295a27..d5e2a336d 100644 --- a/rover/config/rbac/role.yaml +++ b/rover/config/rbac/role.yaml @@ -132,8 +132,8 @@ rules: - apichangelogs - apispecifications - eventspecifications - - mcpspecifications - filespecifications + - mcpspecifications - roadmaps - rovers verbs: @@ -150,8 +150,8 @@ rules: - apichangelogs/finalizers - apispecifications/finalizers - eventspecifications/finalizers - - mcpspecifications/finalizers - filespecifications/finalizers + - mcpspecifications/finalizers - roadmaps/finalizers - rovers/finalizers verbs: @@ -162,8 +162,8 @@ rules: - apichangelogs/status - apispecifications/status - eventspecifications/status - - mcpspecifications/status - filespecifications/status + - mcpspecifications/status - roadmaps/status - rovers/status verbs: From 85e967d2eeb7ff52627c57309f4b794bd4136c6f Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Tue, 4 Aug 2026 16:30:02 +0300 Subject: [PATCH 22/31] feat(rover): lint fix --- rover-server/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/rover-server/README.md b/rover-server/README.md index e5852d402..083cc9d31 100644 --- a/rover-server/README.md +++ b/rover-server/README.md @@ -52,5 +52,3 @@ Please review the official viper documentation for more details: https://github. ## Installation See [kustomize](./config/default/kustomization.yaml) for the default installation configuration. And [installation](../install/overlays/default/kustomization.yaml) for more details on how to deploy it with the entire Controlplane. - - From 7bac2f9277b7d2b2a30822ae7969b7abf348bf7c Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Tue, 4 Aug 2026 17:13:53 +0300 Subject: [PATCH 23/31] feat: fix failing tests --- rover-server/internal/api/server.gen.go | 509 ++++++------------ .../internal/mapper/rover/in/file_test.go | 11 +- .../mapper/rover/in/suite_rover_in_test.go | 2 - tools/e2e-tester/go.mod | 6 +- 4 files changed, 169 insertions(+), 359 deletions(-) diff --git a/rover-server/internal/api/server.gen.go b/rover-server/internal/api/server.gen.go index 5fbb87149..0ae648b26 100644 --- a/rover-server/internal/api/server.gen.go +++ b/rover-server/internal/api/server.gen.go @@ -1,7 +1,3 @@ -// Copyright 2026 Deutsche Telekom IT GmbH -// -// SPDX-License-Identifier: Apache-2.0 - // Package api provides primitives to interact with the openapi HTTP API. // // Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.6.1-0.20260425061340-097bc33cf46c DO NOT EDIT. @@ -306,21 +302,6 @@ func (e FileExposureInfoVariant) Valid() bool { } } -// Defines values for FileSubscriptionVariant. -const ( - FileSubscriptionVariantSftp FileSubscriptionVariant = "sftp" -) - -// Valid indicates whether the value is a known member of the FileSubscriptionVariant enum. -func (e FileSubscriptionVariant) Valid() bool { - switch e { - case FileSubscriptionVariantSftp: - return true - default: - return false - } -} - // Defines values for FileSubscriptionInfoType. const ( FileSubscriptionInfoTypeFile FileSubscriptionInfoType = "file" @@ -336,21 +317,6 @@ func (e FileSubscriptionInfoType) Valid() bool { } } -// Defines values for FileSubscriptionInfoVariant. -const ( - FileSubscriptionInfoVariantSftp FileSubscriptionInfoVariant = "sftp" -) - -// Valid indicates whether the value is a known member of the FileSubscriptionInfoVariant enum. -func (e FileSubscriptionInfoVariant) Valid() bool { - switch e { - case FileSubscriptionInfoVariantSftp: - return true - default: - return false - } -} - // Defines values for GrantType. const ( CLIENTCREDENTIALS GrantType = "CLIENT_CREDENTIALS" @@ -1271,30 +1237,18 @@ type FileSubscription struct { FileType string `json:"fileType"` PublicKeys []PublicKey `json:"publicKeys"` Type string `json:"type"` - - // Variant File-transfer backend. Optional; currently only "sftp" is supported. - Variant FileSubscriptionVariant `json:"variant,omitempty,omitzero"` } -// FileSubscriptionVariant File-transfer backend. Optional; currently only "sftp" is supported. -type FileSubscriptionVariant string - // FileSubscriptionInfo defines model for FileSubscriptionInfo. type FileSubscriptionInfo struct { FileType string `json:"fileType"` PublicKeys []PublicKey `json:"publicKeys"` Type FileSubscriptionInfoType `json:"type"` - - // Variant File-transfer backend. Optional; currently only "sftp" is supported. - Variant FileSubscriptionInfoVariant `json:"variant,omitempty,omitzero"` } // FileSubscriptionInfoType defines model for FileSubscriptionInfo.Type. type FileSubscriptionInfoType string -// FileSubscriptionInfoVariant File-transfer backend. Optional; currently only "sftp" is supported. -type FileSubscriptionInfoVariant string - // GrantType defines model for GrantType. type GrantType string @@ -2509,309 +2463,166 @@ func (t *SubscriptionInfo) UnmarshalJSON(b []byte) error { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "HMY4qYLM/ujdfbz7/wEAAP//HIH+Oi0tAQA=", - "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", - "EQEA", + "H4sIAAAAAAAC/+x9a3fbOLLgX8Hhvedsei4lOememR3Pl3VsuaOdxPaV5Omem2R9ILIkYUICbACUo+7j", + "/74HD75BiZIfSRx9iiPiUShUFeqFwh9ewOKEUaBSeMd/eEvAIXD9569jEGkkT1lKpfp/CCLgJJGEUe/Y", + "OwlkiiNE03gGHLE5IhJigTjIlFMIEaFILgFxEAmjAtCMhWs0ZxzJJREowQvoe74ngiXEWI0eE0riNPaO", + "j3xPrhPwjj1CJSyAe3d3vvfrlEkctcCivzVAibEMloQuUMCJBE7wDhPe+V6COY5BWmScJOR0iekCIrYY", + "hQ4QloBSSn5LAZFQwaCXyUGwlAeA5iyK2K0CRiElwVICp2jBWZr0ehJw3OvNsIArLJee7xE1YmL+pjhW", + "oOHq/L7H4beUcAi9Y8lTKC/MLkZITuhCI+8kIWOGwxgnXwj0YvadAZ8kEJA5CbCC9nHAz9peKHjbllCH", + "Y7eFnKZcMN4EPtC/I8nQDFAqINQskuAFoXqiPhrNEWUSJZytSAihr5cxJ1xIzUTolkSR6pwxXh8phNhx", + "iUAYGTCQXGKJEkaoFGo+bLqzOQqx1Kyh1/1bCnxdLNyM421e3FsSEwdX6vEF+V3B9lsKQkKIZmsNf8Co", + "SGPgf0dxKqRe4AxQhPkClIDARnbE+DOJcYTygVqAjPT8ZRhDmOM0kt7xqyPfmzMeY2nY+8dXnu/pcRXn", + "q6+5GHjplDtjtgL+pciO28l3o7XJEqu5R3TOmmCP5kiPoMESEstUIEyVuDZoIoyqxeAoQiKd5TCKnM5Y", + "TKRsyvc+uqSR2dzyUHjGUql/xUkSWe5BRAqI5k7SJXYmQRZUsxuV0RpxCNMAqgeKpiwFuiQx9NGZ2XJF", + "8nMcCWijaFHCjpNkdO+cFmaMRYCpQSzjDjKPiJAKZXMCUSjqrCwYl4QuKvDR0obXoVNTlMECqmjzvfn+", + "0W9s952iDYMRfU69xuHYcJv6X8CoBHNilvA/SDibRRD/17+FWsEfpen+k8PcO/b+Y1BoBQPzVQyGnDN7", + "NlYx8Prk7GY8/O/r4WTq3fneOeMzEoZAnw6C88vx69HZ2fBCzX/B5DlLafh0019cTm/OL68vztT0E+Ar", + "4Kbtk0EwGY7/ORzfDMfjy7EC4priVC4ZJ7/DE+Lh+uLkevrmcjz6n+GZgUKkScK4hPAdhARPNfE+HTST", + "66ury/F0eHbzbng2Opn+62qoGdl21XodGX5OmEi5BizhLAEuieElnKhTF0fbIDix7SaSYwmLtVp6rhI1", + "RbTvBYQHKZGvOeBPwLcNf1ptfed7c0widTRs63metbvzvYjh8DWOMA0UEFs6vq00VjIGS8iP+U09x1nD", + "U0YlJtRMziFmK3hT2BY1W0JJZIHWLFXiU2hNa40wykSrMUq03BRGjymODjVwiGYwZ1yduvwW8zA7dq3e", + "oQZV/313eoWE5k4lfZWN4Nwd+wPmHOutFBCknMj1tqVPsnZqDJ4qfWcK2EySz7ZpgGnRyQWHtNzTADhN", + "hOSqk+vjCnOCnUaTxQhY+kdZSz8/dN6dXnm+Nx2+HZ5eXkyHv07VD81DyPdWRJAZiTog6Z9FS3N2ZXrN", + "ezOqX3BdZdgSRxVrKkBhs39DIBUoBUNnChCOosu5d/x+CxMXguDOr0uCDPUZYjBxnsVueCbprIT3+tAb", + "BcU+jL4XubpJy71BOcTbl7vrFlRQ9bDbULKfN29Ck0tOrkaoRH3wGcdJpIYfACUDCUL2cEIGq5eegzVy", + "zq+O+9aKtiADCq2AC6UcA5VcgeV3ExoVx4QELTliQkem78u6GKltadly1z224e6UA5ZQUjE7bm0Z/Y6N", + "tdKrjnulzCu1WmnNTNkXuV69TGc9EbAEQoSDAIRWvJVewVmkhb1UxqREamRjnUFunPXROeMIVsDXUjuI", + "QGn8iEgUYKpOFbKgTCHIdzBEDTsfa/jRW9CgrxBLaK5vDBFgAcoAB6TMfuCEhRUKe3X06qfe0Y+9l392", + "0VZluProZ8X/CkITxm4jIiO3ymyZSEBz8hmMXZgA19YcDQCRWElmiLWv0HUK2CEboEwgxlSSIKdx46qr", + "zP2y/6r/o+d71lT2jr3/9+FD+F8fPvRL//znhmmvedSc+VL/gSN0PX6rKIRblFMmNYsV8y+lTMTxYGB/", + "6QcsHtjWYrDKoMt9CSknTvIoM5fe9QIt1f3axmhKQIytZdckqJuI0E9bZcNb3agshHYWKTkIdw5JsnEB", + "ZeAfREgQhxfmutUDU9ncit8FJ6RnTXAOOLyk0TpzqzRoy5jl9VkvcAyZUMkFeGXCXeYwXpitx7Vp5RJB", + "VaojYeZO8B3ivbZJ10l4f3FuBz0YUt+LIWWd0d+VGfWUFk7L2bCnYZPsZNkkO+nUNrDzNWnU3ICk3cIR", + "obCHQp2Fqx5NnbYT7K9MZ3j/hlTpkqq11pou3UG/Lu9IR+16mu3/BvX6v18ipWHvoVtLTCIIUVhVsnXU", + "ikQgJKNVDWREiSQ4yhXQWyKXKNCiFrBMOTg1aklk5Fjau2wKZBqUJ3r3zytkDYvWEberyoouYgVdKZiz", + "VV2OV8meKnK2jA4KsqWEL6seWyA2KscVnayDMNhLX26XA/fQluN1T8mEXry2GnO87lmp+jA6czFYZdId", + "5ri3zlxB/f5acI7/j45shSZdivpnHIbEsN1VqaFZdE21QysckVAdnb3KMEpoM6uqGfVskZJQiz5t7bME", + "KE6IkoDiFi8WwNGLEBIOAZYQ/oAsvzZQVCPhKugtpFxZ/v6nWxWL38sZV1n1l5VvFVAeRsq5h2ysTBHl", + "gvF1c3tP7ZdcdilVtYzyn29OrkZOhXEnUdhHp2ZbNfFIhkLCIZDRGuEw5Jp0VPOMHzqKUKCkN2est/rR", + "28/NoJkpVItGL0Q6E6CVXRL+UJl38zTfkvjZVcb73gpoyPhJQpqY/GUJcgk8pxudGEWR6WEJyZHw0eZW", + "yYm0PGkdvfkCujDE/idQTVZmsuS7ijkl9wk6JduiThlC1Ljdpeh5qdcmN8R+hneeNJEttuZws8kf2G2+", + "XAGPidCeeJ5GINRJOCeLlNvT0rJJPkln07k8bdvCgyWsOKOlNTj8WOVksRCoJHMC3ORGFrD/loIy7NEL", + "+C3FkUCEE3EaEaBypMVi011opnaaPzZAoPOxSpOINoRsNnN8DzhnvDu5XJncFxfCsgB998EqjiLHiBlF", + "DymeReA4HUc0VCtVEnyOstZKaILp0YaUugz1PbIT02zAQnlznXKraDCBgIPNeMgOGvepdpZ/P0bXApDQ", + "PRXa+kFpKESokICzBEWjY9Y9p4QijOap1KkUJuTTd2ojnIiREClwS4fOFlP2CeiQhjpht61hpjY0PrCZ", + "yXNp65gI4lKKriYjJNZCQpwl05eYD0uJg6VRiRxbX+ggV5NR7+XRj3/+21+q4Tz1+/uj3t8+/vGXO2cg", + "r8B+h3Mka2kUBb7AEjZjNWt1lc4iIpbDFbQjNmu74XsH1eRyBRxHUaGhiNJB050pGmebgzt0Rsws2kFG", + "nGGJXSPdYk4JXTwIz/7OaOsRXzXwy0e+VbJ+Ny40i+qaAHCwe52zWviovgtl+VrGYy7BSyjJxFnLmVyN", + "XZVzir3J6N3V22Epterkenrp+cXv55fX4+G/hhNnepU6VxUftrkVSpw4UUauO1amkaWGegdyycIqgDMs", + "SODV5eNpphcIZLojXAGlqvyPz0/RX/7609/66Bcil0gPqZR9PccPvrJhtDhRZzXmStpSmeWRv5lOr1BF", + "f7DBqr8bV+mMhevNI2QxKtWyX8L0xeWFQvDV5WTq+d7rk8noVCmULLQhFxI4cZ4wId+yBUvlGIwFeM2j", + "KldsDXklStQEhkpLHUrHI99zbGfwvaF9NQklaIqe7asolKHuPoQyLIW62Sa+chvaBQ9nUYsU2YyC2rQP", + "g4yHgvS1IjwFbhOuBAtxy3i4AZ7CbJjl47iIOBXAW5QEt52Vd/ALMFzS7rTkpmnMetqIyhemWHWp0KaA", + "nkd4oZVPxdn/YHSBbKgfzcyoWdgEiSVLozDz17js+HqCRDapc10RJg7X4gkShC4iQIH6jlY4SqGPhp+x", + "9gsxqt00+lfEuPnjnLPY3G6aKTkllUiqLl43c80VEakUh8pkzsSnbB7XIIkSlXNCIUT2SpIi3choqtZT", + "U5sgo6krG94vnban9sJW6afXTZN8E8FrzArX3c1PQA0kAt1yIiXQAsIkwlJZWr0YU7yAEL19N0FSdemj", + "DMyeICFof+8xWjLtikXqaKI6zJyd7v9LoMxLYS48LYqWqKwSlBraS03G88XMMZMDgF5QJs3hhCmCz0rN", + "xREanV2Z75nSoSjCHIXq7PyhSQc4DbdmrmiydOPV7sy4nFjSzfVRdOkWPipdeRud5X7T7C4feiGXQLhV", + "FJwW+J0jV+ljlzUVOTBNh7Fte5apMt3XnPd1kKVqh/TFQuMByN29eZ/ybbUZoBi4ok9ND4pOrNqDeD5Q", + "uYPEn0CxaKD0mQCQtq9bun3o7H9p0kInxUGbAg3EthqXudzqwPX5Nagads9Pe3/930d/1Y4nxmNkehh9", + "TvPuGmn1u8EsoY68O+HSHU5Z6IbaXM/r7r5TzTe5I6iQmLboAYV5aD/ZfNdyRL/1ZO90TpthXLyj7doN", + "qXiFx9/YwcBH4S46kO8ZEWAzYpT5duffK8MPFMRlhWIjiWftso7TtnQuHe7bwWemBjPWk8tb+5SJaE+R", + "a1bgbisR7eZPr9Lf9oQzDUhnz3dpl7oLLMnJYrE9oKHHntq2bmmmZ98c4i8HMLthrETSW2OeDgh0yGdj", + "ttKJUpK5rCQrqTNtzVId1zbp/IUD74SaX5FCgHX1AvotJcEnITGXJsL3wAHGPP3o/04uL5DGDbiighn9", + "VHv/K1+KAfqFgARzHTHVJ3LIpKhGSkPoS4jgE4v7QEm/WF3fnXVYutxQvrRw1D/amuRkWbH7PYDmLu+Z", + "S+Eil+8hm6K57i+YT9EEZrdLFRv6PywhuHT+OmMpIdvkLiyWUOOvRgZECD3LcDoZomC4nma4L3JNosGZ", + "pch+hUmd27Bn0L5NhpvfNwbuAxxFMxx86nAZ4jKRl6l0FgDRcr9cjyNkIHQRFgoQGs9A2duiuDRzt7A5", + "esM4+Z1RX18zUEzLEolYavJRJJqtkQApzdUBokWD2lFjxjQ9oCFERAmDVj0OlIUQwM8g30iZWGwbF/Y5", + "428AR3J5ugSDlA3FTpa6JQpU09xqs9fbMkLG6OfhNPck26ifroeC3gxPzrIvLUvRbDIGCVTNPyWunJ6R", + "rgCSMCHILAKFnIjd1m2/bAidw66m/ysK8Vr00QQAvQ9ZID6+yBJwQ1hBpGgkP85CGKgmA8GDgcQ8JOIm", + "SIVkMfCbJabhjLFPg6XZxIHd154yQFcEbgenuulNvowbtY7BDx+oM8hd1sBbEqzyAKI9CAph4hoxweuI", + "4bCVGjhYgiEgroBPIGC0xVVhy/wUyC4KhJVHUTSAhB5HL3JTQSA1v+RrPIvACBplbm64i6CjGrbKTqCa", + "mks3mJsSSCQEDiHKx6yZ+s3ZG3dqmjbOVg/2XtrwjuZpQRc1Bq9ucLvus3c6T1OINs85S/uVSdzedkfL", + "tpDwA9g102Jn6kb7CtMAwglEoIMW5ySSpmFjpKwaUNFkp+BGqe8760opgoSEBlGqNf9slaVfPmd/jS5O", + "316fDT3fG/5q/vroDPY31uI2FtrhLmUqNxFa8oCERO1eTCiWxhkV4ySxl/cw6VAQwtfZWR3uV1nK72Yb", + "+96caFWpxQEVQcmMzihifWEKRekF3/keo7DHHbDdDPgdymZsdqmVV/Rxw6blpcvuuXG2yFe3zbONu2+g", + "7dB1E7PY50NtpB1vh83s1ONktwmaK1Sq7Hkph7Qqyn5n1HVg2lhO6aqqbohulyRYakVTnZnYqkypMG6J", + "PFW1dGi6LutZyeF2XW5MaTfgumi1kt/ZWGZbClcXK0dnHqmN3rFrlm2zpeFdq61VWlzZ+928b9fug0/a", + "sorb/N51/aHVr12RHg2QFBO26owmDeMfsN4hqSnrspvjtq3WkgK+JzmmYg4cKQsOaNhHmc/r7yhIOQdq", + "gsnRGn3wxFwmHzxlK+Ql1MohWvX50Ssx5UitoHDb5uymr9WOuW3eYi1suypVauzN9vTXQDYPgfv99eUG", + "jh54D37muLAQ8wSDk8nkl8vxmed7p29Hw4vpzel4eDa8mI5O3k483xsPz8fDyZub6eU/hhdOMjdmf3ND", + "lVXccrPBpSWOkjGo70UmUE3xjiJ2e9+8rLeZb7EqE96cTIeXJxOkXY+1sr+NsCeFzw6xoszp6/Eod9Gr", + "VqZarvqlqLpbDOzra/d5MkPRRIeaTSpIh+R2AdF8OzhWqG2GaNcbw3pq3yDERXBv6+U5agSSE84mrrDk", + "pVfKV84EgabGYpuWFZRqrZCOUXxTQHTrbbsMMhcS3gXJQ95Eve/N0Do4jWjGVwTbFwwU1EHZ8eKln0HX", + "ZZndbmC2XKXsfhHhPlvZ1eU/tpEl45ErkLbhAt8ON/XqiGs4/b8g7V7iVC5fObYwz7XbmlQmitTwFh+Y", + "+ah0mvavxY2bRoNFWQHYBE6hKdz5m3NgOcw5iKW2r7qmfmx3i5atNa0bbjkDdYcSHdRK3pnNgzyJtZY4", + "H2KJS7mrOvHJ5jxWUglXBNtEeIEYz3Ld9WMaeWDVgFAYBza33XTzfO/15dm/PN97Mzw5G46d6lRdxWOG", + "sB4gpdhJtpV7MU3UGX95o6Q+pqGuS0+k0GVUVpjKapH6imNSZ20aRSoAIcwBPNdlWzzfm0Us+KT/SoCG", + "5qMiygh0QZLQXD6h6h8XFlpt4wCnouX+BaPSanGOYjPKonYTagxC4EVbNKTIRu8iIscwb2xTNr5vYXft", + "2FWOQ7U5FVKhBlMFFt34DlsxmZtMDVx+AkcZgMnkDTIWEfoE6ywV+51NtLbpqQlwlFlQztTpCM/AcQn0", + "TRpj2uOAQ+1qql09Vdz2Cda7zlbDt5na16tz4bqSyFu/iZxGcsoi4LjtQsmShGCStEtF4hzNWMorQi57", + "G6MZc4oJTc2Wd2gs8njc1sZ3m9ZeSfjtmEbVnjTctGnzCnbd04Xd9m2Zsxz3ssg/i7ShBhF+InRHdUoX", + "7ktwS9pp0mr4brpuVwLSgpSrScV0m5Ze07scZW7tix25JDde3cDsj2h5MESUjgHY8RSoyWRTO+NEuouE", + "odsl0Ookt1hkFTfK9qnS/HqSxLD5wrc7Fmy+mwAw1y9rQagwEel6ilZkNqoy3fvadHb/2g2U/vzUMLH6", + "+b/TJVoL2M77GWEhUd6587YmzaNvy7Irza0VA13ylqB+/9a9Z1mLp922unEiTeW2Onrqu+sUHO7QEG7c", + "ct12w7DU+s5vFr14uJoV+5dg2Kv8QunarSLjs+lpUYRBKSLFvRVnyYVAsmqaqvql9/LVjz/9uXojX/+u", + "r+S7K2sba7+UhpnOfnplyii/6vXiNU46FWsjDS/rJszVfLJfb7WCHcsT3f/y/6Yr9qXS07diqw76e1Ub", + "r7HmnsnGY1sNqC0vv5wbaiinaYd+06nI9RRTW8Gg9nRXDdlK5LQ740pUu0sBt2p5IJdAoyvCGY2rF9NL", + "iU/pzO01cddH3qJilmczY9uR/OoCW5H0BZ2yev7OnlgLyqbyv9UBn4677nRmhh7GuOvGTGq8nwQBJBLC", + "LgiuOTtbPDi2llLCoig3nLmdDeUFPXYL+7SrE2XvZTMLNPuaMb+zYJHT27LRNWDblewVoiv8Kk0sW6rn", + "78gnQXG7uvDKbHDqOzazaYzVXcMPiKygUW6pMaANAb6gcPtDVkPE1PnJg43NYU2n4eeEcBAuNf+XTMPP", + "xrdD6rRu0P20oVg4WPWe7KD2P4iNcg+jwcJ7uhXDCYcVYalAL2wXJ5p9e4k9pZJEGTIKBPuuWha1Nu2b", + "YJuiFywKf8imFZIlAs1AnavYSpiO2O/OJXtbH3te3WgTwtsYoV1/icmCG7mYpDxhwj67k91PyKsUyCXw", + "WyKq1yiqon1SKZ+1G4AFJ31DPPqd80iDsielKpybUmeLmjEtZF4Up/GzUFCb5DNf75vWmg+zuVkBl9MX", + "axMnGsS/8WGPWyCLpXRdt6hpIPkozqlbwiJFGKoIOOVhJlc4RA/UwsOtoaX2ENEWUZp1bFtS6lD6dixs", + "WSxok3fy/kN9ST/dfaHfskvNUy7zvmnZ4Ny8Wtrjvpn0lXG2ZdLXGm/PpK912JZJX/eF3DeTvpH3uPvF", + "oh2fs9wtE3Pbxt73mkRjrF02uPN1CUenXTb6oa5NOMbcccM7Xp/YuYt7xR/rMuHO98oFRRxVHmyREuPr", + "IgJh85cOE6j/RhG7NR7RzFVWiZEQuVS2LHxWNh+RqFQ6xF0wYEv6R9tZ+c9Kinpxu2x4MR2Or8ajSblO", + "ZuXHXy7Hb8883/ufy4uhu+xYVrlroutHaGgvT7IkqVrBCf27VQRvypUl5xG7VVQasduS8nxatDCPk2c5", + "RuaO7TEOY0KPcRSpvVB/lxySOIpQOT5Z6cJm81TYIsW2Z/FT10EUreTd1X+2d1ymswxcd1Nz8dm664o+", + "FXgvt0LaMooFeLwBVEdPkSbAV0Qwng1QeUxgd+wpKu2CBeuuLPfaCw+OcTpjoto3FcAt5FUc7DhEZRmV", + "kfZbkx7UtTkdF5il1ulrT853ppSxMOCAo1iYfwaDhDPJAhYNWAKUhL2AUQqBHOiRKtaStTTurPZpS8hJ", + "HGgbAGJ9s8k7f/f6ZnoyPhtNbibm/s3/Ka7XZz79Y8808XwvrQC75518e9Nn0Hh+zDu5GmUuOTNlHuJI", + "IkzBFtzVca6Tq1FRmcFUTTQFH7TvQvio/maE+ck+ZCR8pI+jSgNtpjerVoh+Xojs2HhVkHkHIy+V4/1o", + "6+N81o+uFpnF3ojOORaSp4G0QVL7TojtZF8CXmoBO8AJyZ9Z1b8swHnJyqaGlF8ntZQ1NSSq9gRn9729", + "n0GeRFGltZ6W4xikTp16X58jSLlgvHlbg6iP+u2AgjhMW6Wt64PedWJ+LO5c63W9OjrKCNJGZkoe2MG/", + "hVHmi/G6vtFaiZ9o2q+dhf9QW/CTmd01aA7l4DUOM7eZ7vJye5drmoXmITSdftze6ZzxGQlDoKbHT9t7", + "XDB5zlJqpnj55y5w5Xfr3kFIcJZ8/OcueDDOBlPf8K7yBMr7Qud431QOXOd240hunpTNA8txiHxUBCXS", + "OMaaydwMoTsqNnpffRT/oy167UhH0PFghBGF28pYDY4yLWtNbILyaxauH4W4q+Hqu7u7uwZXvXqUiTdx", + "1Mnp6fBqOjw78NVT81WDh3KOqbHHJrJu4ZA7v3YYDf7A5cf/wzvDPdq953g1VP2uCxNvYiLTrNakdiq5", + "cFg0qVDpKPQc58xPDvCGb4cHiv2aKbaVgFoF+gZFiSL4TISu07WRHJWW9Li0ePTk0vmg7zxffaedrtvV", + "nrRd62EcpTpYXGc7lOos2xCCCCt2WQG6up72G/xjQs0Pz0KPq1RVA+Rfi1KVxUgPzPuVK1WtXHMf3WpQ", + "5Jvtfq5pJwYHmXKq747kaWgbT7tJ1urrPfNarz8fTr3DqddK9ZuZ0L7E3s3XlrkOu3jasrbPy89mV3Xw", + "sn3vXrYSeZcYzP7awcNGS4OUr261eNmygR9NHbQTPLWHzU578K89C/9amajbGKN27mjVz37dxalWzNLi", + "Uisa7KzQ5eAc3GnP0Z22hUB3cKW1EaExLR6LAo+eWAAflJnvxZjYyhlO99l1bv534Y3cSfaA7PGYGtFT", + "u8cOGtFzOHC28cQeqtE+PrHMwNjFI2b77O8Pe6LD6+ANOxxgu9L7JrarFGHs4g+7GqFa9lwHt1i1x87s", + "VTziu6XhqXGVdWg5YVw+uprZXnjVza9ZTUMNy6+9sS590ztlKW1999d2GPxqGpu2d773a2/KJI46dtZt", + "bd+7g9T4CqRGI3W+NUV8F0FTS2/vJIBak8KbmdqOHOnWXOwNXsaGtCgJsGrZ56rHMUvFdj/AWXHXVIbp", + "I/1N31KloQnsqf8U2dtEICzWNFhyRlkqfFvhjQh0u1yXq7kRgRZkBRRhoWbCVOjHzxjC6OpyMh1cXefP", + "r/lIEBoAIrUX05ZYVi+uSIZmkJVrQ0tsnrJbg0QzAFqU/EK2o35UzY5gCtORvNpbH43mZXjxXAI3Hwmj", + "qiExFSwAfShVxfzg+VWYsof/f0uBk+x1uZ+HUwWsAF2iTC6Bq8WVas0hker0+HkaRWvEeN7M3Lkc5JW/", + "MIfSW/klgHP4M5AHNgyrIQ8Y5xBIA2upUphdbHbpXICuRWtvrPbRZXb5XP36wdb0/OD1ka3U+uroJYoB", + "U+Gb3bmF/DZzuUKtJh41FGj4KUNirRPvSaDWKiDGVP1tFuujmSmswcE8MhgEqXnxRdfFsqs2+1MspY+m", + "GoJsJUt2W8MQIvoBxBwoW+aCs4QThai8OFC7231Sqx79SKbmhpLpj29wthQiP5id34LZ2Tx5Nrnm6+S8", + "4TRxKsXaIq203OKy/9OfRrHCGqby+E9/QlP99GUEmYQNGYXKcRKtleABKlLukv9KgOJAplgJTTNn6Osn", + "GPSFGFt/yDzsabuYEg9GDARAVoAw+unop/xE6H+gFedwZXW6AkdLfKGOy53N5BoeD7GGZ8uD7QS2TaPr", + "7Oipj+py7NzT9OxGskdf5Gg6eHgOttqXs9U2s+Jmk617QOVgqB0MtYOh1hJMfCRt7PGtvacOLx6svees", + "aXY4OR7K7NsnHFkZY6egZKXn/qHJJ1VgDwHK56O+PrWWuque2SXi6eJ1XdRr18ino0DI1tCno8/3Efxs", + "LvwQ/jzIpINJvTX86ZYYmTxrft0zBNoc6GBbH2zrg21tWcTBZ49jGDcnetJAaHP6g3H8bEOhTqLeeLK0", + "6MqDP6DR+nmFRJvYaA2KOrG68e6vWrx9WZWEjWeAs0vA+rHL/A6wC+FeuR6tOe53uSF8CLM+szDr7vzd", + "NdTqHLlh736znHD0hc7TgwPsYGx+FfHbfSRH1xjuwdI8WJoHS7M1ivstHZpPYf4+aWT4YP5+j7Hhx7aD", + "d44RN+fsHiVu9m2LEz9z1fsQdz6o3d+82r01oN0qnuIgaQ1ouyLT74LknnHpLNz8mExdh/JQge57vPP+", + "M0gdG313eoVEW2i0TiqVwKgrxtPo8Dgqbn2aJ43v1Cc/qLfPpOJcgxU2c4LzhBj8kal7jeCNK9Dh5Jdt", + "EYULhk4tIR/I6uuvE7cjWfmtGsZ2Yjn6IkLuoCA8WwVhD+q9h4WM5iyK2K1S47X3E0sJnKIFZ2nS6ylI", + "e72srX7S1WlRFwJ4Vzvaut9dLsUvpNg8qefuoNg808JxD67YNL1xWw+r3IN2cFYdDqxHO7BKMSQ2/76O", + "L8W1XD/Xuv3ahXnWdetNC9Ps+7hcodd6uE/xIOLr4Dr/pq5H5GyeCUb9w16XIMxYh2SUQzLKIRnFMoVh", + "pscxF/XYT+r81jMeDMNnep8BZcRaPwkK5XJA6Jy1aphjwCFSLbRuGUWoRGnb9M1Sy5Ga46tRO/2mHh0p", + "KTJbl5eHlP4uWp4by74VTEYkxMKhyPvZD5hzvFaUsB3KJVZ2h0ba4+vIapqDjnzQkb8LHbm7PNsgMf/Q", + "/z6jy1x6jahntCYOMVuB6sd1IfsBmqSzfGUCDdDwc8IUmKL1vld27Owm8I04OpS7fP73sFr1kq6XrbL+", + "Da3jwSnv6OlU7YMb+3AofhU5l+3c2fVC08FtdHAbHdxG5YSDhziYHsnh9KRJCQeH03dyg6iD6yk3pHZx", + "QuXh37LttsUBtZf/Kee9b9Bjc+Cdgx75hZ0rbXzaSR4ICLgRBQmWwbIpE0aUSHXuCjWRbow4k0bbmltv", + "SEkA9LUDJm9BBFpiGkYQopnRM9VeCaG+3cJsydgnq4qGEJAQRK5RSYYS4HPGY4TRguMA5mlUDPyCRWEO", + "D8SYUIFWOCIhClOuFWHTSQ1CWPiDUmAwooz2mmPNsFI+mVFCfmcUUMDonCxSI+b6Du/LGIQ1gScGgQ9o", + "CD+wCmAAHNvFnli9b6NikOmGvcaOE0sN4XMTfEd/24D1hLNZBPF/7Xif2wq5Bn5PGZ1HJJCo5+ApZalF", + "ShqslVqccLbgIMTzV2w0gRqzL4gIUGnoVssY/WrLnqJt2/3nsb7fKMzEKedAZWNPrGHSBMVXNluUhkrg", + "ZILLJa2MWZptpl+2LpU5HDC6Ar6A0M9MMMQhghWmEsHnhPC1tomFxHEiXOLo54owynh9zxrdT+ejc4H7", + "XelbXx9XOrSStmzRrkzSmV93LFRggimdaxMYivuaWeKQhn2wO75B//XWMgE51++AdzOz3jRXPvnJ1Ugd", + "otw4MoXI/bnaU7iCsI8mhlgEilnerCcCloDoe76X8sg79pZSJuJ4MPhjyYS8MzJpsPrR870V5gTPIsPm", + "yzybc47TSHrHHk5IX0IEn1jcD8HzHfnuE4n5Quk0qrevvcrvQxaIjy+yWUNYQaQEVWmogWoyEDwYGJzc", + "BKmQLAZ+owypGWOfBmrgn7GEmyFdEc5oDFTeXK4UwcHt4D+EnbeHadgjnIgftAxppOQDjnsKj9olDCtQ", + "WgbgGMV4rePdOJUsxpIEOIrWxtsrMpwyGpnWPYNYZfYRocPYWoEVTA2hjT6N014goy1Yxwl5hmj/mLNB", + "Hf/vMMULcL4wbXOfGu9FNDfRDuKu026HcVTpaB3IVYjDDtO49NE6yOkS0wVEbFHqnf/W3i3PprZdjNho", + "bX6SkDHDYYyTKsrsj97dx7v/HwAA///J9/1FbzMBAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/rover-server/internal/mapper/rover/in/file_test.go b/rover-server/internal/mapper/rover/in/file_test.go index 3061ba6c2..94233db43 100644 --- a/rover-server/internal/mapper/rover/in/file_test.go +++ b/rover-server/internal/mapper/rover/in/file_test.go @@ -35,10 +35,11 @@ var _ = Describe("File Type (SFTP) Mapper", func() { Expect(output.PublicKeys[0].Key).To(Equal("ssh-ed25519 AAAA1")) }) - It("must leave visibility empty when omitted (CRD default applies at admission)", func() { - // The mapper does not default visibility itself — consistent with - // mapApiExposure/mapEventExposure. An empty value is passed through and - // the CRD's +kubebuilder:default=Enterprise fills it in at admission. + It("must default visibility to Enterprise when omitted", func() { + // The mapper defaults visibility via the shared toRoverVisibility, + // which maps an empty value to Enterprise — consistent with + // mapApiExposure/mapEventExposure and the CRD's + // +kubebuilder:default=Enterprise. input := api.FileExposure{ Type: "file", FileType: "demo-sftp-spec-v1", @@ -49,7 +50,7 @@ var _ = Describe("File Type (SFTP) Mapper", func() { output := mapFileExposure(input) - Expect(output.Visibility).To(BeEmpty()) + Expect(output.Visibility).To(Equal(roverv1.VisibilityEnterprise)) }) It("must map ZONE visibility to the CRD Zone visibility", func() { diff --git a/rover-server/internal/mapper/rover/in/suite_rover_in_test.go b/rover-server/internal/mapper/rover/in/suite_rover_in_test.go index f5680961c..26a542323 100644 --- a/rover-server/internal/mapper/rover/in/suite_rover_in_test.go +++ b/rover-server/internal/mapper/rover/in/suite_rover_in_test.go @@ -43,7 +43,6 @@ var ( fileExposure = api.FileExposure{ Type: "file", FileType: "demo-sftp-spec-v1", - Variant: "sftp", Visibility: "World", PublicKeys: []api.PublicKey{ {Label: "provider-key", Key: "ssh-ed25519 AAAA-provider"}, @@ -53,7 +52,6 @@ var ( fileSubscription = api.FileSubscription{ Type: "file", FileType: "demo-sftp-spec-v1", - Variant: "sftp", PublicKeys: []api.PublicKey{ {Label: "consumer-key", Key: "ssh-ed25519 AAAA-consumer"}, }, diff --git a/tools/e2e-tester/go.mod b/tools/e2e-tester/go.mod index f8b718b2b..bbb552603 100644 --- a/tools/e2e-tester/go.mod +++ b/tools/e2e-tester/go.mod @@ -54,12 +54,12 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.53.0 // indirect - golang.org/x/mod v0.36.0 // indirect + golang.org/x/mod v0.37.0 // indirect golang.org/x/net v0.56.0 // indirect golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect - golang.org/x/text v0.38.0 // indirect - golang.org/x/tools v0.45.0 // indirect + golang.org/x/text v0.39.0 // indirect + golang.org/x/tools v0.47.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) From ee6e8447356c99c74067d8c39f58c639325b2ceb Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Tue, 4 Aug 2026 17:15:06 +0300 Subject: [PATCH 24/31] feat: sum forgotten --- tools/e2e-tester/go.sum | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/e2e-tester/go.sum b/tools/e2e-tester/go.sum index 707e57154..95bdd8eba 100644 --- a/tools/e2e-tester/go.sum +++ b/tools/e2e-tester/go.sum @@ -122,6 +122,7 @@ golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGb golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= @@ -133,8 +134,10 @@ golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From dbd17cdfc5dc806ce8b920abdebcdc48230b397a Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Tue, 4 Aug 2026 17:26:39 +0300 Subject: [PATCH 25/31] feat: add test file add header --- ...r.cp.ei.telekom.de_filespecifications.yaml | 2 +- .../handler/filespecification/handler_test.go | 127 ++++++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 rover/internal/handler/filespecification/handler_test.go diff --git a/rover/config/crd/bases/rover.cp.ei.telekom.de_filespecifications.yaml b/rover/config/crd/bases/rover.cp.ei.telekom.de_filespecifications.yaml index 2d78187c8..8f7d89251 100644 --- a/rover/config/crd/bases/rover.cp.ei.telekom.de_filespecifications.yaml +++ b/rover/config/crd/bases/rover.cp.ei.telekom.de_filespecifications.yaml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2026 Deutsche Telekom IT GmbH +# SPDX-FileCopyrightText: 2025 Deutsche Telekom IT GmbH # # SPDX-License-Identifier: Apache-2.0 --- diff --git a/rover/internal/handler/filespecification/handler_test.go b/rover/internal/handler/filespecification/handler_test.go new file mode 100644 index 000000000..ef895e142 --- /dev/null +++ b/rover/internal/handler/filespecification/handler_test.go @@ -0,0 +1,127 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package filespecification + +import ( + "context" + "testing" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + commonclient "github.com/telekom/controlplane/common/pkg/client" + "github.com/telekom/controlplane/common/pkg/config" + filev1 "github.com/telekom/controlplane/file/api/v1" + roverv1 "github.com/telekom/controlplane/rover/api/v1" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestFileSpecificationHandler(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "FileSpecification Handler Suite") +} + +const testEnvironment = "test" + +var _ = Describe("FileSpecificationHandler", func() { + var ( + ctx context.Context + fakeClient ctrlclient.Client + handler *FileSpecificationHandler + ) + + newFileSpec := func(name string) *roverv1.FileSpecification { + return &roverv1.FileSpecification{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "default"}, + Spec: roverv1.FileSpecificationSpec{ + Description: "demo file type", + Specification: "file-id-123", + StorageType: roverv1.FileStorageTypeSFTP, + }, + } + } + + // newContext returns a context carrying a fresh JanitorClient over the shared + // fake client, so AnyChanged() reflects only the operations of one reconcile. + newContext := func() context.Context { + scoped := commonclient.NewScopedClient(fakeClient, testEnvironment) + janitor := commonclient.NewJanitorClient(scoped) + return commonclient.WithClient(logr.NewContext(ctx, logr.Discard()), janitor) + } + + BeforeEach(func() { + ctx = context.Background() + scheme := runtime.NewScheme() + Expect(roverv1.AddToScheme(scheme)).To(Succeed()) + Expect(filev1.AddToScheme(scheme)).To(Succeed()) + + fakeClient = fake.NewClientBuilder().WithScheme(scheme).Build() + handler = &FileSpecificationHandler{} + }) + + getFileType := func(name string) *filev1.FileType { + fileType := &filev1.FileType{} + Expect(fakeClient.Get(ctx, types.NamespacedName{Name: name, Namespace: "default"}, fileType)).To(Succeed()) + return fileType + } + + It("should create a FileType from the FileSpecification and mark it provisioning", func() { + fileSpec := newFileSpec("demo-sftp-spec-v1") + + Expect(handler.CreateOrUpdate(newContext(), fileSpec)).To(Succeed()) + + fileType := getFileType("demo-sftp-spec-v1") + Expect(fileType.Spec.Type).To(Equal("demo-sftp-spec-v1")) + Expect(fileType.Spec.Description).To(Equal("demo file type")) + Expect(fileType.Spec.Specification).To(Equal("file-id-123")) + Expect(fileType.Labels).To(HaveKey(filev1.FileTypeLabelKey)) + Expect(fileType.Labels).To(HaveKeyWithValue(config.EnvironmentLabelKey, testEnvironment)) + Expect(fileType.OwnerReferences).To(HaveLen(1)) + Expect(fileType.OwnerReferences[0].Name).To(Equal("demo-sftp-spec-v1")) + + // Status references the created FileType. + Expect(fileSpec.Status.FileType.Name).To(Equal("demo-sftp-spec-v1")) + Expect(fileSpec.Status.FileType.Namespace).To(Equal("default")) + + // First reconcile changed the cluster, so it is not yet ready. + ready := meta.FindStatusCondition(fileSpec.Status.Conditions, "Ready") + Expect(ready).NotTo(BeNil()) + Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + }) + + It("should mark the FileSpecification ready when nothing changed (idempotent)", func() { + fileSpec := newFileSpec("demo-sftp-spec-v1") + Expect(handler.CreateOrUpdate(newContext(), fileSpec)).To(Succeed()) + + // Second reconcile with a fresh janitor client: no change expected. + fileSpec = newFileSpec("demo-sftp-spec-v1") + Expect(handler.CreateOrUpdate(newContext(), fileSpec)).To(Succeed()) + + ready := meta.FindStatusCondition(fileSpec.Status.Conditions, "Ready") + Expect(ready).NotTo(BeNil()) + Expect(ready.Status).To(Equal(metav1.ConditionTrue)) + }) + + It("should normalize the FileType name derived from the specification name", func() { + fileSpec := newFileSpec("De.Telekom.Foo.v1") + + Expect(handler.CreateOrUpdate(newContext(), fileSpec)).To(Succeed()) + + // dots -> hyphens and lower-cased. + getFileType("de-telekom-foo-v1") + Expect(fileSpec.Status.FileType.Name).To(Equal("de-telekom-foo-v1")) + }) + + It("should return nil on Delete", func() { + Expect(handler.Delete(newContext(), newFileSpec("demo-sftp-spec-v1"))).To(Succeed()) + }) +}) From c805a59717c18c4acd5964551ca927822206d602 Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Tue, 4 Aug 2026 20:06:11 +0300 Subject: [PATCH 26/31] feat: add test file --- .../internal/handler/rover/file/util_test.go | 68 +++++++------------ 1 file changed, 26 insertions(+), 42 deletions(-) diff --git a/rover/internal/handler/rover/file/util_test.go b/rover/internal/handler/rover/file/util_test.go index 9a8c52bb4..1bf1c9526 100644 --- a/rover/internal/handler/rover/file/util_test.go +++ b/rover/internal/handler/rover/file/util_test.go @@ -5,59 +5,43 @@ package file import ( - "testing" - roverv1 "github.com/telekom/controlplane/rover/api/v1" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) -func TestMakeName(t *testing.T) { - tests := []struct { - name string - fileType string - owner string - want string - }{ - {"hyphenated file type", "de-telekom-eni-foo-v1", "provider", "de-telekom-eni-foo-v1--provider"}, - {"dotted file type is normalized", "de.telekom.foo.v1", "consumer", "de-telekom-foo-v1--consumer"}, - {"mixed case is lowercased", "De.Telekom.V1", "app", "de-telekom-v1--app"}, - {"owner name is normalized", "de.telekom.foo.v1", "My_App", "de-telekom-foo-v1--my-app"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := MakeName(tt.fileType, tt.owner); got != tt.want { - t.Errorf("MakeName(%q, %q) = %q, want %q", tt.fileType, tt.owner, got, tt.want) - } - }) - } -} +var _ = Describe("MakeName", func() { + DescribeTable("normalizes file type and owner into a resource name", + func(fileType, owner, want string) { + Expect(MakeName(fileType, owner)).To(Equal(want)) + }, + Entry("hyphenated file type", "de-telekom-eni-foo-v1", "provider", "de-telekom-eni-foo-v1--provider"), + Entry("dotted file type is normalized", "de.telekom.foo.v1", "consumer", "de-telekom-foo-v1--consumer"), + Entry("mixed case is lowercased", "De.Telekom.V1", "app", "de-telekom-v1--app"), + Entry("owner name is normalized", "de.telekom.foo.v1", "My_App", "de-telekom-foo-v1--my-app"), + ) +}) -func TestMapPublicKeys(t *testing.T) { - t.Run("nil input yields nil", func(t *testing.T) { - if got := mapPublicKeys(nil); got != nil { - t.Errorf("mapPublicKeys(nil) = %v, want nil", got) - } +var _ = Describe("mapPublicKeys", func() { + It("yields nil for nil input", func() { + Expect(mapPublicKeys(nil)).To(BeNil()) }) - t.Run("empty slice yields nil", func(t *testing.T) { - if got := mapPublicKeys([]roverv1.PublicKey{}); got != nil { - t.Errorf("mapPublicKeys([]) = %v, want nil", got) - } + It("yields nil for an empty slice", func() { + Expect(mapPublicKeys([]roverv1.PublicKey{})).To(BeNil()) }) - t.Run("maps label and key preserving order", func(t *testing.T) { + It("maps label and key preserving order", func() { in := []roverv1.PublicKey{ {Label: "provider-key", Key: "ssh-ed25519 AAAA"}, {Label: "consumer-key", Key: "ssh-ed25519 BBBB"}, } got := mapPublicKeys(in) - if len(got) != 2 { - t.Fatalf("mapPublicKeys len = %d, want 2", len(got)) - } - if got[0].Label != "provider-key" || got[0].Key != "ssh-ed25519 AAAA" { - t.Errorf("got[0] = %+v", got[0]) - } - if got[1].Label != "consumer-key" || got[1].Key != "ssh-ed25519 BBBB" { - t.Errorf("got[1] = %+v", got[1]) - } + Expect(got).To(HaveLen(2)) + Expect(got[0].Label).To(Equal("provider-key")) + Expect(got[0].Key).To(Equal("ssh-ed25519 AAAA")) + Expect(got[1].Label).To(Equal("consumer-key")) + Expect(got[1].Key).To(Equal("ssh-ed25519 BBBB")) }) -} +}) From e67fe8c6c9c6217ff3244b85c4dfcb601071a997 Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Wed, 5 Aug 2026 10:37:56 +0300 Subject: [PATCH 27/31] feat: enable file feature --- common/pkg/config/feature.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/common/pkg/config/feature.go b/common/pkg/config/feature.go index d65263b0e..3fe3da163 100644 --- a/common/pkg/config/feature.go +++ b/common/pkg/config/feature.go @@ -49,11 +49,7 @@ var ( FeatureSecretManager Feature = NewFeature("secret_manager", true) // Secret Manager feature enabled by default FeatureFileManager Feature = NewFeature("file_manager", true) // File Manager feature enabled by default FeatureAiGateway Feature = NewFeature("ai_gateway", false) // AI Gateway (MCP) feature disabled by default - // TODO(DHEI-20905): File/SFTP domain feature disabled by default. The rover-domain - // dispatch is in place, but the File domain (DHEI-20903) and SFTP/DDS domain - // (DHEI-20904) are not yet available. Enable this flag once those domains land so - // the rover operator can create file-domain resources instead of returning Blocked. - FeatureFile Feature = NewFeature("file", false) + FeatureFile Feature = NewFeature("file", true) ) // SetFeatureEnabled sets the enabled state for a feature. Intended for tests. From 0c2cd81dd33e2525bb9eb5478c4d2a5c4206be0e Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Thu, 6 Aug 2026 17:11:52 +0300 Subject: [PATCH 28/31] feat: add/modify tests --- rover/internal/controller/index.go | 14 ++ rover/internal/controller/suite_test.go | 5 + .../handler/rover/file/handlers_test.go | 128 ++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 rover/internal/handler/rover/file/handlers_test.go diff --git a/rover/internal/controller/index.go b/rover/internal/controller/index.go index ed0bb3a3e..7f494669d 100644 --- a/rover/internal/controller/index.go +++ b/rover/internal/controller/index.go @@ -16,6 +16,7 @@ import ( cconfig "github.com/telekom/controlplane/common/pkg/config" "github.com/telekom/controlplane/common/pkg/controller/index" eventv1 "github.com/telekom/controlplane/event/api/v1" + filev1 "github.com/telekom/controlplane/file/api/v1" permissionv1 "github.com/telekom/controlplane/permission/api/v1" ) @@ -80,4 +81,17 @@ func RegisterIndicesOrDie(ctx context.Context, mgr ctrl.Manager) { os.Exit(1) } } + + if cconfig.FeatureFile.IsEnabled() { + err = index.SetOwnerIndex(ctx, mgr.GetFieldIndexer(), &filev1.FileExposure{}) + if err != nil { + ctrl.Log.Error(err, "unable to create ownerIndex for FileExposure") + os.Exit(1) + } + err = index.SetOwnerIndex(ctx, mgr.GetFieldIndexer(), &filev1.FileSubscription{}) + if err != nil { + ctrl.Log.Error(err, "unable to create ownerIndex for FileSubscription") + os.Exit(1) + } + } } diff --git a/rover/internal/controller/suite_test.go b/rover/internal/controller/suite_test.go index 81aaf841f..ee35bec6b 100644 --- a/rover/internal/controller/suite_test.go +++ b/rover/internal/controller/suite_test.go @@ -28,6 +28,7 @@ import ( apiapi "github.com/telekom/controlplane/api/api/v1" applicationv1 "github.com/telekom/controlplane/application/api/v1" "github.com/telekom/controlplane/common/pkg/test/mock" + filev1 "github.com/telekom/controlplane/file/api/v1" organizationv1 "github.com/telekom/controlplane/organization/api/v1" roverv1 "github.com/telekom/controlplane/rover/api/v1" secretsapi "github.com/telekom/controlplane/secret-manager/api" @@ -81,6 +82,7 @@ var _ = BeforeSuite(func() { filepath.Join("..", "..", "..", "api", "config", "crd", "bases"), filepath.Join("..", "..", "..", "application", "config", "crd", "bases"), filepath.Join("..", "..", "..", "organization", "config", "crd", "bases"), + filepath.Join("..", "..", "..", "file", "config", "crd", "bases"), ), // CRDDirectoryPaths: append( // testutil.GetCrdPathsOrDie("github.com/telekom/controlplane/(api|application|organization)/api"), @@ -108,6 +110,9 @@ var _ = BeforeSuite(func() { err = organizationv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = filev1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + // +kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) diff --git a/rover/internal/handler/rover/file/handlers_test.go b/rover/internal/handler/rover/file/handlers_test.go new file mode 100644 index 000000000..3baaf671b --- /dev/null +++ b/rover/internal/handler/rover/file/handlers_test.go @@ -0,0 +1,128 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package file + +import ( + "context" + "testing" + + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + commonclient "github.com/telekom/controlplane/common/pkg/client" + "github.com/telekom/controlplane/common/pkg/config" + "github.com/telekom/controlplane/common/pkg/util/contextutil" + filev1 "github.com/telekom/controlplane/file/api/v1" + roverv1 "github.com/telekom/controlplane/rover/api/v1" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestFileHandlers(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "File Handler Suite") +} + +const ( + testEnvironment = "test" + testZone = "cetus" +) + +var _ = Describe("File Exposure/Subscription Handlers", func() { + var ( + ctx context.Context + fakeClient ctrlclient.Client + ) + + newOwner := func() *roverv1.Rover { + return &roverv1.Rover{ + ObjectMeta: metav1.ObjectMeta{Name: "my-app", Namespace: "default"}, + Spec: roverv1.RoverSpec{Zone: testZone}, + } + } + + newJanitor := func() commonclient.JanitorClient { + scoped := commonclient.NewScopedClient(fakeClient, testEnvironment) + return commonclient.NewJanitorClient(scoped) + } + + BeforeEach(func() { + scheme := runtime.NewScheme() + Expect(roverv1.AddToScheme(scheme)).To(Succeed()) + Expect(filev1.AddToScheme(scheme)).To(Succeed()) + + fakeClient = fake.NewClientBuilder().WithScheme(scheme).Build() + // Env is required by the exposure handler (zone namespace resolution). + ctx = contextutil.WithEnv(logr.NewContext(context.Background(), logr.Discard()), testEnvironment) + }) + + Context("HandleExposure", func() { + It("should create a file-domain FileExposure owned by the Rover", func() { + owner := newOwner() + exp := &roverv1.FileExposure{ + FileType: "demo-sftp-spec-v1", + Visibility: roverv1.VisibilityWorld, + Approval: roverv1.Approval{Strategy: roverv1.ApprovalStrategyAuto}, + PublicKeys: []roverv1.PublicKey{{Label: "provider-key", Key: "ssh-ed25519 AAAAprovider"}}, + } + + Expect(HandleExposure(ctx, newJanitor(), owner, exp)).To(Succeed()) + + name := MakeName(exp.FileType, owner.Name) + fileExposure := &filev1.FileExposure{} + Expect(fakeClient.Get(ctx, types.NamespacedName{Name: name, Namespace: "default"}, fileExposure)).To(Succeed()) + + Expect(fileExposure.Spec.FileType).To(Equal("demo-sftp-spec-v1")) + Expect(fileExposure.Spec.Visibility).To(Equal(filev1.Visibility("World"))) + Expect(fileExposure.Spec.Approval.Strategy).To(Equal(filev1.ApprovalStrategy("Auto"))) + Expect(fileExposure.Spec.Sftp.PublicKeys).To(HaveLen(1)) + Expect(fileExposure.Spec.Sftp.PublicKeys[0].Label).To(Equal("provider-key")) + Expect(fileExposure.Spec.Zone.Name).To(Equal(testZone)) + Expect(fileExposure.Spec.Zone.Namespace).To(Equal(testEnvironment)) + + Expect(fileExposure.Labels).To(HaveKeyWithValue(filev1.FileTypeLabelKey, "demo-sftp-spec-v1")) + Expect(fileExposure.Labels).To(HaveKeyWithValue(config.BuildLabelKey("application"), "my-app")) + Expect(fileExposure.Labels).To(HaveKeyWithValue(config.EnvironmentLabelKey, testEnvironment)) + Expect(fileExposure.OwnerReferences).To(HaveLen(1)) + Expect(fileExposure.OwnerReferences[0].Name).To(Equal("my-app")) + + Expect(owner.Status.FileExposures).To(HaveLen(1)) + Expect(owner.Status.FileExposures[0].Name).To(Equal(name)) + }) + }) + + Context("HandleSubscription", func() { + It("should create a file-domain FileSubscription owned by the Rover", func() { + owner := newOwner() + sub := &roverv1.FileSubscription{ + FileType: "demo-sftp-spec-v1", + PublicKeys: []roverv1.PublicKey{{Label: "consumer-key", Key: "ssh-ed25519 AAAAconsumer"}}, + } + + Expect(HandleSubscription(ctx, newJanitor(), owner, sub)).To(Succeed()) + + name := MakeName(sub.FileType, owner.Name) + fileSubscription := &filev1.FileSubscription{} + Expect(fakeClient.Get(ctx, types.NamespacedName{Name: name, Namespace: "default"}, fileSubscription)).To(Succeed()) + + Expect(fileSubscription.Spec.FileType).To(Equal("demo-sftp-spec-v1")) + Expect(fileSubscription.Spec.Sftp.PublicKeys).To(HaveLen(1)) + Expect(fileSubscription.Spec.Sftp.PublicKeys[0].Label).To(Equal("consumer-key")) + + Expect(fileSubscription.Labels).To(HaveKeyWithValue(filev1.FileTypeLabelKey, "demo-sftp-spec-v1")) + Expect(fileSubscription.Labels).To(HaveKeyWithValue(config.BuildLabelKey("zone"), testZone)) + Expect(fileSubscription.OwnerReferences).To(HaveLen(1)) + Expect(fileSubscription.OwnerReferences[0].Name).To(Equal("my-app")) + + Expect(owner.Status.FileSubscriptions).To(HaveLen(1)) + Expect(owner.Status.FileSubscriptions[0].Name).To(Equal(name)) + }) + }) +}) From eb28a5f828328c6f305785772eb94374b69f3169 Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Thu, 6 Aug 2026 17:15:08 +0300 Subject: [PATCH 29/31] feat: back reuse compl --- rover-server/internal/api/server.gen.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rover-server/internal/api/server.gen.go b/rover-server/internal/api/server.gen.go index 0ae648b26..3bc848d8a 100644 --- a/rover-server/internal/api/server.gen.go +++ b/rover-server/internal/api/server.gen.go @@ -1,3 +1,7 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + // Package api provides primitives to interact with the openapi HTTP API. // // Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.6.1-0.20260425061340-097bc33cf46c DO NOT EDIT. From 8b801e8c7b5315991eb761a32f6f096a84059024 Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Thu, 6 Aug 2026 18:21:28 +0300 Subject: [PATCH 30/31] feat: changed rbac group name --- file/api/go.mod | 74 ---- file/api/go.sum | 211 ---------- file/api/go.sum.license | 3 - file/api/v1/file_shared_types.go | 61 --- file/api/v1/fileexposure_types.go | 113 ----- file/api/v1/filesubscription_types.go | 97 ----- file/api/v1/filetype_types.go | 109 ----- file/api/v1/groupversion_info.go | 24 -- file/api/v1/suite_test.go | 17 - file/api/v1/types_test.go | 109 ----- file/api/v1/zz_generated.deepcopy.go | 389 ------------------ .../file.ei.telekom.de_fileexposures.yaml | 212 ---------- .../file.ei.telekom.de_filesubscriptions.yaml | 140 ------- .../bases/file.ei.telekom.de_filetypes.yaml | 128 ------ file/config/crd/kustomization.yaml | 13 - rover/config/rbac/role.yaml | 2 +- .../filespecification_controller.go | 2 +- rover/internal/controller/rover_controller.go | 4 +- 18 files changed, 4 insertions(+), 1704 deletions(-) delete mode 100644 file/api/go.mod delete mode 100644 file/api/go.sum delete mode 100644 file/api/go.sum.license delete mode 100644 file/api/v1/file_shared_types.go delete mode 100644 file/api/v1/fileexposure_types.go delete mode 100644 file/api/v1/filesubscription_types.go delete mode 100644 file/api/v1/filetype_types.go delete mode 100644 file/api/v1/groupversion_info.go delete mode 100644 file/api/v1/suite_test.go delete mode 100644 file/api/v1/types_test.go delete mode 100644 file/api/v1/zz_generated.deepcopy.go delete mode 100644 file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml delete mode 100644 file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml delete mode 100644 file/config/crd/bases/file.ei.telekom.de_filetypes.yaml delete mode 100644 file/config/crd/kustomization.yaml diff --git a/file/api/go.mod b/file/api/go.mod deleted file mode 100644 index 231177c0a..000000000 --- a/file/api/go.mod +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2026 Deutsche Telekom IT GmbH -// -// SPDX-License-Identifier: Apache-2.0 - -module github.com/telekom/controlplane/file/api - -go 1.26.5 - -require ( - github.com/onsi/ginkgo/v2 v2.32.0 - github.com/onsi/gomega v1.42.1 - github.com/telekom/controlplane/common v0.0.0 - k8s.io/apiextensions-apiserver v0.36.2 - k8s.io/apimachinery v0.36.2 - sigs.k8s.io/controller-runtime v0.24.1 -) - -require ( - github.com/Masterminds/semver/v3 v3.4.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.13.0 // indirect - github.com/evanphx/json-patch/v5 v5.9.11 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.23.0 // indirect - github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/go-viper/mapstructure/v2 v2.5.0 // indirect - github.com/google/gnostic-models v0.7.0 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/sagikazarmark/locafero v0.12.0 // indirect - github.com/spf13/afero v1.15.0 // indirect - github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect - github.com/spf13/viper v1.21.0 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/x448/float16 v0.8.4 // indirect - go.yaml.in/yaml/v2 v2.4.4 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.38.0 // indirect - golang.org/x/net v0.56.0 // indirect - golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/term v0.44.0 // indirect - golang.org/x/text v0.39.0 // indirect - golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.47.0 // indirect - google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect - gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.36.2 // indirect - k8s.io/client-go v0.36.2 // indirect - k8s.io/klog/v2 v2.140.0 // indirect - k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect - k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect - sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect - sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect - sigs.k8s.io/yaml v1.6.0 // indirect -) - -replace github.com/telekom/controlplane/common => ../../common diff --git a/file/api/go.sum b/file/api/go.sum deleted file mode 100644 index 68bec845a..000000000 --- a/file/api/go.sum +++ /dev/null @@ -1,211 +0,0 @@ -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= -github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= -github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= -github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= -github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= -github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= -github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= -github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= -github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= -github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= -github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= -github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= -github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= -github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= -github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= -github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= -github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= -github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.28.3 h1:4JvMdwtFU0imd8fHx25OJXoDMRexnf8v5NHKYSTTji4= -github.com/onsi/ginkgo/v2 v2.28.3/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= -github.com/onsi/ginkgo/v2 v2.32.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= -github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc= -github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= -github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= -github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= -github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= -github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= -github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= -github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= -github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= -github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= -github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= -github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= -go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= -go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= -go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= -go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= -golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= -golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= -golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= -golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= -golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= -golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= -golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= -google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= -google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= -gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80= -k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34= -k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg= -k8s.io/apiextensions-apiserver v0.36.0 h1:Wt7E8J+VBCbj4FjiBfDTK/neXDDjyJVJc7xfuOHImZ0= -k8s.io/apiextensions-apiserver v0.36.0/go.mod h1:kGDjH0msuiIB3tgsYRV0kS9GqpMYMUsQ3GHv7TApyug= -k8s.io/apiextensions-apiserver v0.36.2/go.mod h1:cL1tBWe8XSaP1H30iWKGo7hf6iAUUUJPEU70dskmAnA= -k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ= -k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc= -k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4= -k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c= -k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y= -k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0= -k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= -k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= -k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= -k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= -k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= -k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= -sigs.k8s.io/controller-runtime v0.24.0 h1:Ck6N2LdS8Lovy1o25BB4r1xjvLEKUl1s2o9kU+KWDE4= -sigs.k8s.io/controller-runtime v0.24.0/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= -sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= -sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= -sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= -sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= -sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= -sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/file/api/go.sum.license b/file/api/go.sum.license deleted file mode 100644 index be863cd5c..000000000 --- a/file/api/go.sum.license +++ /dev/null @@ -1,3 +0,0 @@ -Copyright 2026 Deutsche Telekom IT GmbH - -SPDX-License-Identifier: Apache-2.0 diff --git a/file/api/v1/file_shared_types.go b/file/api/v1/file_shared_types.go deleted file mode 100644 index 72dcf735e..000000000 --- a/file/api/v1/file_shared_types.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2026 Deutsche Telekom IT GmbH -// -// SPDX-License-Identifier: Apache-2.0 - -package v1 - -// PublicKey is a labeled SSH public key registered on the SFTP user. -type PublicKey struct { - // Label is a human-readable identifier for the key. It must be unique per file type. - // +kubebuilder:validation:Required - // +kubebuilder:validation:MinLength=1 - Label string `json:"label"` - - // Key is the SSH public key value. It must be unique per file type. - // +kubebuilder:validation:Required - // +kubebuilder:validation:MinLength=1 - Key string `json:"key"` -} - -// Visibility defines who can see and subscribe to an exposed file type. -// +kubebuilder:validation:Enum=World;Zone;Enterprise -type Visibility string - -const ( - VisibilityWorld Visibility = "World" - VisibilityZone Visibility = "Zone" - VisibilityEnterprise Visibility = "Enterprise" -) - -func (v Visibility) String() string { - return string(v) -} - -// ApprovalStrategy defines the approval mode for subscriptions to a file type exposure. -// +kubebuilder:validation:Enum=Auto;Simple;FourEyes -type ApprovalStrategy string - -const ( - ApprovalStrategyAuto ApprovalStrategy = "Auto" - ApprovalStrategySimple ApprovalStrategy = "Simple" - ApprovalStrategyFourEyes ApprovalStrategy = "FourEyes" -) - -func (a ApprovalStrategy) String() string { - return string(a) -} - -// Approval configures how subscriptions to a file type exposure are approved. -type Approval struct { - // Strategy defines the approval mode for subscriptions to this file type. - // +kubebuilder:validation:Enum=Auto;Simple;FourEyes - // +kubebuilder:default=Simple - Strategy ApprovalStrategy `json:"strategy"` - - // TrustedTeams identifies teams that are trusted for approving subscriptions - // to this file type. Per default your own team is trusted. - // +kubebuilder:validation:Optional - // +kubebuilder:validation:MinItems=0 - // +kubebuilder:validation:MaxItems=10 - TrustedTeams []string `json:"trustedTeams,omitempty"` -} diff --git a/file/api/v1/fileexposure_types.go b/file/api/v1/fileexposure_types.go deleted file mode 100644 index 230dada4f..000000000 --- a/file/api/v1/fileexposure_types.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2026 Deutsche Telekom IT GmbH -// -// SPDX-License-Identifier: Apache-2.0 - -package v1 - -import ( - ctypes "github.com/telekom/controlplane/common/pkg/types" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// FileExposureSpec defines the desired state of FileExposure. -type FileExposureSpec struct { - // Approval configures how subscriptions to this file type are approved. - Approval Approval `json:"approval"` - - // Visibility defines who can see and subscribe to this file type. - // +kubebuilder:default=Enterprise - Visibility Visibility `json:"visibility,omitempty"` - - // FileType is the file type identifier this exposure belongs to. - // References the FileType CR via MakeFileTypeName() conversion. - // +kubebuilder:validation:Required - // +kubebuilder:validation:MinLength=1 - FileType string `json:"fileType"` - - // Sftp holds the SFTP storage-backend-specific configuration for this exposure. - // Backend-specific settings live under their own sub-object (e.g. sftp) so that - // additional storage backends can be added without polluting the spec root. - // +kubebuilder:validation:Required - Sftp SftpExposure `json:"sftp"` - - // Zone references the Zone CR where this file type is exposed. - // On this layer only the Zone ref is passed; the file domain resolves it to - // the zone-scoped service configuration for the backend. - Zone ctypes.ObjectRef `json:"zone"` -} - -// SftpExposure holds the SFTP storage-backend-specific configuration for a FileExposure. -type SftpExposure struct { - // PublicKeys are the SSH public keys registered for the provider's SFTP user. - // +kubebuilder:validation:Required - // +kubebuilder:validation:MinItems=1 - PublicKeys []PublicKey `json:"publicKeys"` -} - -// FileExposureStatus defines the observed state of FileExposure. -type FileExposureStatus struct { - // +listType=map - // +listMapKey=type - // +patchStrategy=merge - // +patchMergeKey=type - // +optional - Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` - - // Active indicates whether this exposure has been provisioned. - Active bool `json:"active,omitempty"` - - // Subscriptions references the file-domain FileSubscriptions bound to this exposure. - // +optional - Subscriptions []ctypes.ObjectRef `json:"subscriptions,omitempty"` -} - -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="FileType",type="string",JSONPath=".spec.fileType",description="The file type identifier" -// +kubebuilder:printcolumn:name="Active",type="boolean",JSONPath=".status.active",description="Whether this exposure is provisioned" -// +kubebuilder:printcolumn:name="CreatedAt",type="date",JSONPath=".metadata.creationTimestamp",description="Creation timestamp" - -// FileExposure is the Schema for the fileexposures API. -// It declares that an application exposes a file type. The derived logical -// Application is created without an Identity client. -type FileExposure struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec FileExposureSpec `json:"spec,omitempty"` - Status FileExposureStatus `json:"status,omitempty"` -} - -var _ ctypes.Object = &FileExposure{} - -func (r *FileExposure) GetConditions() []metav1.Condition { - return r.Status.Conditions -} - -func (r *FileExposure) SetCondition(condition metav1.Condition) bool { - return meta.SetStatusCondition(&r.Status.Conditions, condition) -} - -// +kubebuilder:object:root=true - -// FileExposureList contains a list of FileExposure. -type FileExposureList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []FileExposure `json:"items"` -} - -var _ ctypes.ObjectList = &FileExposureList{} - -func (r *FileExposureList) GetItems() []ctypes.Object { - items := make([]ctypes.Object, len(r.Items)) - for i := range r.Items { - items[i] = &r.Items[i] - } - return items -} - -func init() { - SchemeBuilder.Register(&FileExposure{}, &FileExposureList{}) -} diff --git a/file/api/v1/filesubscription_types.go b/file/api/v1/filesubscription_types.go deleted file mode 100644 index 7d2168e8e..000000000 --- a/file/api/v1/filesubscription_types.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2026 Deutsche Telekom IT GmbH -// -// SPDX-License-Identifier: Apache-2.0 - -package v1 - -import ( - ctypes "github.com/telekom/controlplane/common/pkg/types" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// FileSubscriptionSpec defines the desired state of FileSubscription. -// It is created in the file domain from a rover-domain Rover subscription (1:1). -type FileSubscriptionSpec struct { - // FileType is the file type identifier this subscription belongs to. - // References the FileType CR via MakeFileTypeName() conversion. - // +kubebuilder:validation:Required - // +kubebuilder:validation:MinLength=1 - FileType string `json:"fileType"` - - // Sftp holds the SFTP storage-backend-specific configuration for this subscription. - // Backend-specific settings live under their own sub-object (e.g. sftp) so that - // additional storage backends can be added without polluting the spec root. - // +kubebuilder:validation:Required - Sftp SftpSubscription `json:"sftp"` -} - -// SftpSubscription holds the SFTP storage-backend-specific configuration for a FileSubscription. -type SftpSubscription struct { - // ClientId identifies the consumer application's client on the SFTP backend. - // +optional - ClientId string `json:"clientId,omitempty"` - - // PublicKeys are the SSH public keys registered for the consumer's SFTP user. - // +kubebuilder:validation:Required - // +kubebuilder:validation:MinItems=1 - PublicKeys []PublicKey `json:"publicKeys"` -} - -// FileSubscriptionStatus defines the observed state of FileSubscription. -type FileSubscriptionStatus struct { - // +listType=map - // +listMapKey=type - // +patchStrategy=merge - // +patchMergeKey=type - // +optional - Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` -} - -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="FileType",type="string",JSONPath=".spec.fileType",description="The file type identifier" -// +kubebuilder:printcolumn:name="CreatedAt",type="date",JSONPath=".metadata.creationTimestamp",description="Creation timestamp" - -// FileSubscription is the Schema for the filesubscriptions API. -// It declares that an application consumes a file type. -type FileSubscription struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec FileSubscriptionSpec `json:"spec,omitempty"` - Status FileSubscriptionStatus `json:"status,omitempty"` -} - -var _ ctypes.Object = &FileSubscription{} - -func (r *FileSubscription) GetConditions() []metav1.Condition { - return r.Status.Conditions -} - -func (r *FileSubscription) SetCondition(condition metav1.Condition) bool { - return meta.SetStatusCondition(&r.Status.Conditions, condition) -} - -// +kubebuilder:object:root=true - -// FileSubscriptionList contains a list of FileSubscription. -type FileSubscriptionList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []FileSubscription `json:"items"` -} - -var _ ctypes.ObjectList = &FileSubscriptionList{} - -func (r *FileSubscriptionList) GetItems() []ctypes.Object { - items := make([]ctypes.Object, len(r.Items)) - for i := range r.Items { - items[i] = &r.Items[i] - } - return items -} - -func init() { - SchemeBuilder.Register(&FileSubscription{}, &FileSubscriptionList{}) -} diff --git a/file/api/v1/filetype_types.go b/file/api/v1/filetype_types.go deleted file mode 100644 index 1eb98386a..000000000 --- a/file/api/v1/filetype_types.go +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2026 Deutsche Telekom IT GmbH -// -// SPDX-License-Identifier: Apache-2.0 - -package v1 - -import ( - "strings" - - "github.com/telekom/controlplane/common/pkg/config" - ctypes "github.com/telekom/controlplane/common/pkg/types" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// FileTypeLabelKey is the label used to associate FileExposure/FileSubscription -// resources with their FileType -var FileTypeLabelKey = config.BuildLabelKey("filetype") - -// MakeFileTypeName generates a Kubernetes resource name from a file type identifier. -func MakeFileTypeName(fileType string) string { - return strings.ToLower(strings.ReplaceAll(fileType, ".", "-")) -} - -// FileTypeSpec defines the desired state of FileType. -type FileTypeSpec struct { - // Type is the dot-separated file type identifier (e.g. "de.telekom.eni.invoices.v1"). - // Used to generate the resource name via MakeFileTypeName() conversion. - // +kubebuilder:validation:Required - // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:MaxLength=253 - // +kubebuilder:validation:Pattern=`^[a-z0-9]+(\.[a-z0-9]+)*$` - Type string `json:"type"` - - // Description provides a human-readable summary of this file type. - // +optional - Description string `json:"description,omitempty"` - - // Specification contains the file ID reference from the file manager for the - // optional document that describes this file type. - // +optional - Specification string `json:"specification,omitempty"` -} - -// FileTypeStatus defines the observed state of FileType. -type FileTypeStatus struct { - // +listType=map - // +listMapKey=type - // +patchStrategy=merge - // +patchMergeKey=type - // +optional - Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` - - // Active indicates whether this FileType is the active singleton for its file - // type identifier. When multiple FileTypes exist for the same identifier, only - // the oldest non-deleted one is active. - Active bool `json:"active,omitempty"` -} - -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status -// +kubebuilder:resource:shortName=ftype -// +kubebuilder:printcolumn:name="Type",type="string",JSONPath=".spec.type",description="The file type identifier" -// +kubebuilder:printcolumn:name="Active",type="boolean",JSONPath=".status.active",description="Indicates if this FileType is the active singleton" -// +kubebuilder:printcolumn:name="CreatedAt",type="date",JSONPath=".metadata.creationTimestamp",description="Creation timestamp" - -// FileType is the Schema for the filetypes API. -// It represents a registered file type in the file domain, serving as the canonical -// reference that FileExposure and FileSubscription point to (mirrors event.EventType). -type FileType struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec FileTypeSpec `json:"spec,omitempty"` - Status FileTypeStatus `json:"status,omitempty"` -} - -var _ ctypes.Object = &FileType{} - -func (r *FileType) GetConditions() []metav1.Condition { - return r.Status.Conditions -} - -func (r *FileType) SetCondition(condition metav1.Condition) bool { - return meta.SetStatusCondition(&r.Status.Conditions, condition) -} - -// +kubebuilder:object:root=true - -// FileTypeList contains a list of FileType. -type FileTypeList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []FileType `json:"items"` -} - -var _ ctypes.ObjectList = &FileTypeList{} - -func (r *FileTypeList) GetItems() []ctypes.Object { - items := make([]ctypes.Object, len(r.Items)) - for i := range r.Items { - items[i] = &r.Items[i] - } - return items -} - -func init() { - SchemeBuilder.Register(&FileType{}, &FileTypeList{}) -} diff --git a/file/api/v1/groupversion_info.go b/file/api/v1/groupversion_info.go deleted file mode 100644 index c8295bf23..000000000 --- a/file/api/v1/groupversion_info.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2026 Deutsche Telekom IT GmbH -// -// SPDX-License-Identifier: Apache-2.0 - -// Package v1 contains API Schema definitions for the file v1 API group. -// +kubebuilder:object:generate=true -// +groupName=file.ei.telekom.de -package v1 - -import ( - "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/scheme" -) - -var ( - // GroupVersion is group version used to register these objects - GroupVersion = schema.GroupVersion{Group: "file.ei.telekom.de", Version: "v1"} - - // SchemeBuilder is used to add go types to the GroupVersionKind scheme - SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} - - // AddToScheme adds the types in this group-version to the given scheme. - AddToScheme = SchemeBuilder.AddToScheme -) diff --git a/file/api/v1/suite_test.go b/file/api/v1/suite_test.go deleted file mode 100644 index 8a6ab691d..000000000 --- a/file/api/v1/suite_test.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2026 Deutsche Telekom IT GmbH -// -// SPDX-License-Identifier: Apache-2.0 - -package v1_test - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestFileApiV1(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "File API V1 Suite") -} diff --git a/file/api/v1/types_test.go b/file/api/v1/types_test.go deleted file mode 100644 index 8eef5c01c..000000000 --- a/file/api/v1/types_test.go +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2026 Deutsche Telekom IT GmbH -// -// SPDX-License-Identifier: Apache-2.0 - -package v1_test - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - ctypes "github.com/telekom/controlplane/common/pkg/types" - v1 "github.com/telekom/controlplane/file/api/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -var _ = Describe("MakeFileTypeName", func() { - DescribeTable("converts file type strings to Kubernetes resource names", - func(input, expected string) { - Expect(v1.MakeFileTypeName(input)).To(Equal(expected)) - }, - Entry("file type with dots", "de.telekom.eni.foo.v1", "de-telekom-eni-foo-v1"), - Entry("already hyphenated", "de-telekom-eni-foo-v1", "de-telekom-eni-foo-v1"), - Entry("empty string", "", ""), - Entry("mixed case with dots", "De.Telekom.V1", "de-telekom-v1"), - ) -}) - -var _ = Describe("FileType", func() { - It("gets and sets conditions", func() { - ft := &v1.FileType{ - ObjectMeta: metav1.ObjectMeta{Name: "demo-v1", Namespace: "team-ns"}, - Spec: v1.FileTypeSpec{Type: "demo-v1", Description: "demo"}, - } - Expect(ft.GetConditions()).To(BeEmpty()) - changed := ft.SetCondition(metav1.Condition{ - Type: "Ready", - Status: metav1.ConditionTrue, - Reason: "Provisioned", - }) - Expect(changed).To(BeTrue()) - Expect(ft.GetConditions()).To(HaveLen(1)) - Expect(ft.GetConditions()[0].Type).To(Equal("Ready")) - }) - - It("exposes list items via GetItems", func() { - list := &v1.FileTypeList{Items: []v1.FileType{ - {ObjectMeta: metav1.ObjectMeta{Name: "a"}}, - {ObjectMeta: metav1.ObjectMeta{Name: "b"}}, - }} - items := list.GetItems() - Expect(items).To(HaveLen(2)) - Expect(items[0].GetName()).To(Equal("a")) - Expect(items[1].GetName()).To(Equal("b")) - }) -}) - -var _ = Describe("FileExposure", func() { - It("deep-copies spec and status without aliasing", func() { - orig := &v1.FileExposure{ - ObjectMeta: metav1.ObjectMeta{Name: "foo-v1--provider", Namespace: "team-ns"}, - Spec: v1.FileExposureSpec{ - Approval: v1.Approval{Strategy: v1.ApprovalStrategySimple}, - Visibility: v1.VisibilityEnterprise, - FileType: "foo-v1", - Sftp: v1.SftpExposure{ - PublicKeys: []v1.PublicKey{ - {Label: "provider-key", Key: "ssh-ed25519 AAAA"}, - }, - }, - }, - Status: v1.FileExposureStatus{ - Active: true, - Subscriptions: []ctypes.ObjectRef{{Name: "sub", Namespace: "team-ns"}}, - }, - } - - clone := orig.DeepCopy() - Expect(clone).To(Equal(orig)) - - // Mutating the clone must not affect the original. - clone.Spec.Sftp.PublicKeys[0].Key = "changed" - clone.Status.Subscriptions[0].Name = "other" - Expect(orig.Spec.Sftp.PublicKeys[0].Key).To(Equal("ssh-ed25519 AAAA")) - Expect(orig.Status.Subscriptions[0].Name).To(Equal("sub")) - }) - - It("gets and sets conditions", func() { - exp := &v1.FileExposure{} - Expect(exp.SetCondition(metav1.Condition{Type: "Ready", Status: metav1.ConditionFalse, Reason: "Pending"})).To(BeTrue()) - Expect(exp.GetConditions()).To(HaveLen(1)) - }) -}) - -var _ = Describe("FileSubscription", func() { - It("deep-copies public keys without aliasing", func() { - orig := &v1.FileSubscription{ - Spec: v1.FileSubscriptionSpec{ - FileType: "foo-v1", - Sftp: v1.SftpSubscription{ - PublicKeys: []v1.PublicKey{{Label: "consumer-key", Key: "ssh-ed25519 BBBB"}}, - }, - }, - } - clone := orig.DeepCopy() - Expect(clone).To(Equal(orig)) - clone.Spec.Sftp.PublicKeys[0].Label = "changed" - Expect(orig.Spec.Sftp.PublicKeys[0].Label).To(Equal("consumer-key")) - }) -}) diff --git a/file/api/v1/zz_generated.deepcopy.go b/file/api/v1/zz_generated.deepcopy.go deleted file mode 100644 index fc644dcbd..000000000 --- a/file/api/v1/zz_generated.deepcopy.go +++ /dev/null @@ -1,389 +0,0 @@ -//go:build !ignore_autogenerated - -// SPDX-FileCopyrightText: 2026 Deutsche Telekom IT GmbH -// -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by controller-gen. DO NOT EDIT. - -package v1 - -import ( - "github.com/telekom/controlplane/common/pkg/types" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Approval) DeepCopyInto(out *Approval) { - *out = *in - if in.TrustedTeams != nil { - in, out := &in.TrustedTeams, &out.TrustedTeams - *out = make([]string, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Approval. -func (in *Approval) DeepCopy() *Approval { - if in == nil { - return nil - } - out := new(Approval) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PublicKey) DeepCopyInto(out *PublicKey) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicKey. -func (in *PublicKey) DeepCopy() *PublicKey { - if in == nil { - return nil - } - out := new(PublicKey) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SftpExposure) DeepCopyInto(out *SftpExposure) { - *out = *in - if in.PublicKeys != nil { - in, out := &in.PublicKeys, &out.PublicKeys - *out = make([]PublicKey, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SftpExposure. -func (in *SftpExposure) DeepCopy() *SftpExposure { - if in == nil { - return nil - } - out := new(SftpExposure) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SftpSubscription) DeepCopyInto(out *SftpSubscription) { - *out = *in - if in.PublicKeys != nil { - in, out := &in.PublicKeys, &out.PublicKeys - *out = make([]PublicKey, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SftpSubscription. -func (in *SftpSubscription) DeepCopy() *SftpSubscription { - if in == nil { - return nil - } - out := new(SftpSubscription) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FileTypeSpec) DeepCopyInto(out *FileTypeSpec) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileTypeSpec. -func (in *FileTypeSpec) DeepCopy() *FileTypeSpec { - if in == nil { - return nil - } - out := new(FileTypeSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FileTypeStatus) DeepCopyInto(out *FileTypeStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileTypeStatus. -func (in *FileTypeStatus) DeepCopy() *FileTypeStatus { - if in == nil { - return nil - } - out := new(FileTypeStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FileType) DeepCopyInto(out *FileType) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileType. -func (in *FileType) DeepCopy() *FileType { - if in == nil { - return nil - } - out := new(FileType) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *FileType) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FileTypeList) DeepCopyInto(out *FileTypeList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]FileType, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileTypeList. -func (in *FileTypeList) DeepCopy() *FileTypeList { - if in == nil { - return nil - } - out := new(FileTypeList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *FileTypeList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FileExposureSpec) DeepCopyInto(out *FileExposureSpec) { - *out = *in - in.Approval.DeepCopyInto(&out.Approval) - in.Sftp.DeepCopyInto(&out.Sftp) - in.Zone.DeepCopyInto(&out.Zone) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileExposureSpec. -func (in *FileExposureSpec) DeepCopy() *FileExposureSpec { - if in == nil { - return nil - } - out := new(FileExposureSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FileExposureStatus) DeepCopyInto(out *FileExposureStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.Subscriptions != nil { - in, out := &in.Subscriptions, &out.Subscriptions - *out = make([]types.ObjectRef, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileExposureStatus. -func (in *FileExposureStatus) DeepCopy() *FileExposureStatus { - if in == nil { - return nil - } - out := new(FileExposureStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FileExposure) DeepCopyInto(out *FileExposure) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileExposure. -func (in *FileExposure) DeepCopy() *FileExposure { - if in == nil { - return nil - } - out := new(FileExposure) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *FileExposure) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FileExposureList) DeepCopyInto(out *FileExposureList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]FileExposure, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileExposureList. -func (in *FileExposureList) DeepCopy() *FileExposureList { - if in == nil { - return nil - } - out := new(FileExposureList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *FileExposureList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FileSubscriptionSpec) DeepCopyInto(out *FileSubscriptionSpec) { - *out = *in - in.Sftp.DeepCopyInto(&out.Sftp) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSubscriptionSpec. -func (in *FileSubscriptionSpec) DeepCopy() *FileSubscriptionSpec { - if in == nil { - return nil - } - out := new(FileSubscriptionSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FileSubscriptionStatus) DeepCopyInto(out *FileSubscriptionStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSubscriptionStatus. -func (in *FileSubscriptionStatus) DeepCopy() *FileSubscriptionStatus { - if in == nil { - return nil - } - out := new(FileSubscriptionStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FileSubscription) DeepCopyInto(out *FileSubscription) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSubscription. -func (in *FileSubscription) DeepCopy() *FileSubscription { - if in == nil { - return nil - } - out := new(FileSubscription) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *FileSubscription) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FileSubscriptionList) DeepCopyInto(out *FileSubscriptionList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]FileSubscription, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSubscriptionList. -func (in *FileSubscriptionList) DeepCopy() *FileSubscriptionList { - if in == nil { - return nil - } - out := new(FileSubscriptionList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *FileSubscriptionList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} diff --git a/file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml b/file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml deleted file mode 100644 index 5badf8853..000000000 --- a/file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml +++ /dev/null @@ -1,212 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Deutsche Telekom IT GmbH -# -# SPDX-License-Identifier: Apache-2.0 ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.19.0 - name: fileexposures.file.ei.telekom.de -spec: - group: file.ei.telekom.de - names: - kind: FileExposure - listKind: FileExposureList - plural: fileexposures - singular: fileexposure - scope: Namespaced - versions: - - additionalPrinterColumns: - - description: The file type identifier - jsonPath: .spec.fileType - name: FileType - type: string - - description: Whether this exposure is provisioned - jsonPath: .status.active - name: Active - type: boolean - - description: Creation timestamp - jsonPath: .metadata.creationTimestamp - name: CreatedAt - type: date - name: v1 - schema: - openAPIV3Schema: - description: |- - FileExposure is the Schema for the fileexposures API. - It declares that an application exposes a file type. The derived logical - Application is created without an Identity client. - properties: - apiVersion: - type: string - kind: - type: string - metadata: - type: object - spec: - description: ExposureSpec defines the desired state of a file Exposure. - properties: - approval: - description: Approval configures how subscriptions to this file type - are approved. - properties: - strategy: - default: Simple - description: Strategy defines the approval mode for subscriptions - to this file type. - enum: - - Auto - - Simple - - FourEyes - type: string - trustedTeams: - description: |- - TrustedTeams identifies teams that are trusted for approving subscriptions - to this file type. Per default your own team is trusted. - items: - type: string - maxItems: 10 - minItems: 0 - type: array - required: - - strategy - type: object - fileType: - description: |- - FileType is the file type identifier this exposure belongs to. - References the FileType CR via MakeFileTypeName() conversion. - minLength: 1 - type: string - sftp: - description: |- - Sftp holds the SFTP storage-backend-specific configuration for this exposure. - Backend-specific settings live under their own sub-object (e.g. sftp) so that - additional storage backends can be added without polluting the spec root. - properties: - publicKeys: - description: PublicKeys are the SSH public keys registered for - the provider's SFTP user. - items: - description: PublicKey is a labeled SSH public key registered - on the SFTP user. - properties: - key: - description: Key is the SSH public key value. It must be - unique per file type. - minLength: 1 - type: string - label: - description: Label is a human-readable identifier for the - key. It must be unique per file type. - minLength: 1 - type: string - required: - - key - - label - type: object - minItems: 1 - type: array - required: - - publicKeys - type: object - visibility: - default: Enterprise - description: Visibility defines who can see and subscribe to this file - type. - enum: - - World - - Zone - - Enterprise - type: string - zone: - description: |- - Zone references the Zone CR where this file type is exposed. - On this layer only the Zone ref is passed; the file domain resolves it to - the zone-scoped service configuration for the backend. - properties: - name: - type: string - namespace: - type: string - uid: - type: string - required: - - name - - namespace - type: object - required: - - approval - - fileType - - sftp - - zone - type: object - status: - description: ExposureStatus defines the observed state of a file Exposure. - properties: - active: - description: Active indicates whether this exposure has been provisioned. - type: boolean - conditions: - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - format: date-time - type: string - message: - maxLength: 32768 - type: string - observedGeneration: - format: int64 - minimum: 0 - type: integer - reason: - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - enum: - - "True" - - "False" - - Unknown - type: string - type: - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - subscriptions: - description: Subscriptions references the file-domain FileSubscriptions - bound to this exposure. - items: - properties: - name: - type: string - namespace: - type: string - uid: - type: string - required: - - name - - namespace - type: object - type: array - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml b/file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml deleted file mode 100644 index 22f793631..000000000 --- a/file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml +++ /dev/null @@ -1,140 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Deutsche Telekom IT GmbH -# -# SPDX-License-Identifier: Apache-2.0 ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.19.0 - name: filesubscriptions.file.ei.telekom.de -spec: - group: file.ei.telekom.de - names: - kind: FileSubscription - listKind: FileSubscriptionList - plural: filesubscriptions - singular: filesubscription - scope: Namespaced - versions: - - additionalPrinterColumns: - - description: The file type identifier - jsonPath: .spec.fileType - name: FileType - type: string - - description: Creation timestamp - jsonPath: .metadata.creationTimestamp - name: CreatedAt - type: date - name: v1 - schema: - openAPIV3Schema: - description: |- - FileSubscription is the Schema for the filesubscriptions API. - It declares that an application consumes a file type. - properties: - apiVersion: - type: string - kind: - type: string - metadata: - type: object - spec: - description: FileSubscriptionSpec defines the desired state of FileSubscription. - properties: - fileType: - description: |- - FileType is the file type identifier this subscription belongs to. - References the FileType CR via MakeFileTypeName() conversion. - minLength: 1 - type: string - sftp: - description: |- - Sftp holds the SFTP storage-backend-specific configuration for this subscription. - Backend-specific settings live under their own sub-object (e.g. sftp) so that - additional storage backends can be added without polluting the spec root. - properties: - clientId: - description: ClientId identifies the consumer application's client - on the SFTP backend. - type: string - publicKeys: - description: PublicKeys are the SSH public keys registered for - the consumer's SFTP user. - items: - description: PublicKey is a labeled SSH public key registered - on the SFTP user. - properties: - key: - description: Key is the SSH public key value. It must be - unique per file type. - minLength: 1 - type: string - label: - description: Label is a human-readable identifier for the - key. It must be unique per file type. - minLength: 1 - type: string - required: - - key - - label - type: object - minItems: 1 - type: array - required: - - publicKeys - type: object - required: - - fileType - - sftp - type: object - status: - description: FileSubscriptionStatus defines the observed state of FileSubscription. - properties: - conditions: - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - format: date-time - type: string - message: - maxLength: 32768 - type: string - observedGeneration: - format: int64 - minimum: 0 - type: integer - reason: - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - enum: - - "True" - - "False" - - Unknown - type: string - type: - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/file/config/crd/bases/file.ei.telekom.de_filetypes.yaml b/file/config/crd/bases/file.ei.telekom.de_filetypes.yaml deleted file mode 100644 index 4bc7a03e7..000000000 --- a/file/config/crd/bases/file.ei.telekom.de_filetypes.yaml +++ /dev/null @@ -1,128 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Deutsche Telekom IT GmbH -# -# SPDX-License-Identifier: Apache-2.0 ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.19.0 - name: filetypes.file.ei.telekom.de -spec: - group: file.ei.telekom.de - names: - kind: FileType - listKind: FileTypeList - plural: filetypes - singular: filetype - shortNames: - - ftype - scope: Namespaced - versions: - - additionalPrinterColumns: - - description: The file type identifier - jsonPath: .spec.type - name: Type - type: string - - description: Indicates if this FileType is the active singleton - jsonPath: .status.active - name: Active - type: boolean - - description: Creation timestamp - jsonPath: .metadata.creationTimestamp - name: CreatedAt - type: date - name: v1 - schema: - openAPIV3Schema: - description: |- - FileType is the Schema for the filetypes API. - It represents a registered file type in the file domain, serving as the canonical - reference that FileExposure and FileSubscription point to (mirrors event.EventType). - properties: - apiVersion: - type: string - kind: - type: string - metadata: - type: object - spec: - description: FileTypeSpec defines the desired state of FileType. - properties: - description: - description: Description provides a human-readable summary of this - file type. - type: string - specification: - description: |- - Specification contains the file ID reference from the file manager for the - optional document that describes this file type. - type: string - type: - description: |- - Type is the dot-separated file type identifier (e.g. "de.telekom.eni.invoices.v1"). - Used to generate the resource name via MakeFileTypeName() conversion. - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9]+(\.[a-z0-9]+)*$ - type: string - required: - - type - type: object - status: - description: FileTypeStatus defines the observed state of FileType. - properties: - active: - description: |- - Active indicates whether this FileType is the active singleton for its file - type identifier. When multiple FileTypes exist for the same identifier, only - the oldest non-deleted one is active. - type: boolean - conditions: - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - format: date-time - type: string - message: - maxLength: 32768 - type: string - observedGeneration: - format: int64 - minimum: 0 - type: integer - reason: - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - enum: - - "True" - - "False" - - Unknown - type: string - type: - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - type: object - type: object - served: true - storage: true - subresources: - status: {} - diff --git a/file/config/crd/kustomization.yaml b/file/config/crd/kustomization.yaml deleted file mode 100644 index 2a901b41f..000000000 --- a/file/config/crd/kustomization.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2026 Deutsche Telekom IT GmbH -# -# SPDX-License-Identifier: Apache-2.0 - -# This kustomization.yaml is not intended to be run by itself, -# since it depends on service name and namespace that are out of this kustomize package. -# It should be run by config/default -resources: -- bases/file.ei.telekom.de_filetypes.yaml -- bases/file.ei.telekom.de_fileexposures.yaml -- bases/file.ei.telekom.de_filesubscriptions.yaml -# +kubebuilder:scaffold:crdkustomizeresource - diff --git a/rover/config/rbac/role.yaml b/rover/config/rbac/role.yaml index d5e2a336d..ec872ffe1 100644 --- a/rover/config/rbac/role.yaml +++ b/rover/config/rbac/role.yaml @@ -93,7 +93,7 @@ rules: - update - watch - apiGroups: - - file.ei.telekom.de + - file.cp.ei.telekom.de resources: - fileexposures - filesubscriptions diff --git a/rover/internal/controller/filespecification_controller.go b/rover/internal/controller/filespecification_controller.go index 62d612b2e..5c7062983 100644 --- a/rover/internal/controller/filespecification_controller.go +++ b/rover/internal/controller/filespecification_controller.go @@ -35,7 +35,7 @@ type FileSpecificationReconciler struct { // +kubebuilder:rbac:groups=rover.cp.ei.telekom.de,resources=filespecifications,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=rover.cp.ei.telekom.de,resources=filespecifications/status,verbs=get;update;patch // +kubebuilder:rbac:groups=rover.cp.ei.telekom.de,resources=filespecifications/finalizers,verbs=update -// +kubebuilder:rbac:groups=file.ei.telekom.de,resources=filetypes,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=file.cp.ei.telekom.de,resources=filetypes,verbs=get;list;watch;create;update;patch;delete func (r *FileSpecificationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { return r.Controller.Reconcile(ctx, req, &rover.FileSpecification{}) diff --git a/rover/internal/controller/rover_controller.go b/rover/internal/controller/rover_controller.go index 8d8135b01..a1e794a46 100644 --- a/rover/internal/controller/rover_controller.go +++ b/rover/internal/controller/rover_controller.go @@ -58,8 +58,8 @@ type RoverReconciler struct { // +kubebuilder:rbac:groups=agentic.cp.ei.telekom.de,resources=agenticexposures,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=agentic.cp.ei.telekom.de,resources=agenticsubscriptions,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=file.ei.telekom.de,resources=fileexposures,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=file.ei.telekom.de,resources=filesubscriptions,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=file.cp.ei.telekom.de,resources=fileexposures,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=file.cp.ei.telekom.de,resources=filesubscriptions,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=permission.cp.ei.telekom.de,resources=permissionsets,verbs=get;list;watch;create;update;patch;delete From e2571cf9c23852d0e0b4d5c2380a9ff05849d1fb Mon Sep 17 00:00:00 2001 From: andrewRdk Date: Fri, 7 Aug 2026 10:34:07 +0300 Subject: [PATCH 31/31] feat: restore deleted files to make compilation success --- file/api/go.mod | 74 ++++ file/api/go.sum | 211 ++++++++++ file/api/go.sum.license | 3 + file/api/v1/file_shared_types.go | 61 +++ file/api/v1/fileexposure_types.go | 113 +++++ file/api/v1/filesubscription_types.go | 97 +++++ file/api/v1/filetype_types.go | 109 +++++ file/api/v1/groupversion_info.go | 24 ++ file/api/v1/suite_test.go | 17 + file/api/v1/types_test.go | 109 +++++ file/api/v1/zz_generated.deepcopy.go | 389 ++++++++++++++++++ .../file.ei.telekom.de_fileexposures.yaml | 212 ++++++++++ .../file.ei.telekom.de_filesubscriptions.yaml | 140 +++++++ .../bases/file.ei.telekom.de_filetypes.yaml | 128 ++++++ file/config/crd/kustomization.yaml | 13 + 15 files changed, 1700 insertions(+) create mode 100644 file/api/go.mod create mode 100644 file/api/go.sum create mode 100644 file/api/go.sum.license create mode 100644 file/api/v1/file_shared_types.go create mode 100644 file/api/v1/fileexposure_types.go create mode 100644 file/api/v1/filesubscription_types.go create mode 100644 file/api/v1/filetype_types.go create mode 100644 file/api/v1/groupversion_info.go create mode 100644 file/api/v1/suite_test.go create mode 100644 file/api/v1/types_test.go create mode 100644 file/api/v1/zz_generated.deepcopy.go create mode 100644 file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml create mode 100644 file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml create mode 100644 file/config/crd/bases/file.ei.telekom.de_filetypes.yaml create mode 100644 file/config/crd/kustomization.yaml diff --git a/file/api/go.mod b/file/api/go.mod new file mode 100644 index 000000000..231177c0a --- /dev/null +++ b/file/api/go.mod @@ -0,0 +1,74 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +module github.com/telekom/controlplane/file/api + +go 1.26.5 + +require ( + github.com/onsi/ginkgo/v2 v2.32.0 + github.com/onsi/gomega v1.42.1 + github.com/telekom/controlplane/common v0.0.0 + k8s.io/apiextensions-apiserver v0.36.2 + k8s.io/apimachinery v0.36.2 + sigs.k8s.io/controller-runtime v0.24.1 +) + +require ( + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.38.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.39.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.47.0 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.36.2 // indirect + k8s.io/client-go v0.36.2 // indirect + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) + +replace github.com/telekom/controlplane/common => ../../common diff --git a/file/api/go.sum b/file/api/go.sum new file mode 100644 index 000000000..68bec845a --- /dev/null +++ b/file/api/go.sum @@ -0,0 +1,211 @@ +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.28.3 h1:4JvMdwtFU0imd8fHx25OJXoDMRexnf8v5NHKYSTTji4= +github.com/onsi/ginkgo/v2 v2.28.3/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/ginkgo/v2 v2.32.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc= +github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= +github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80= +k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34= +k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg= +k8s.io/apiextensions-apiserver v0.36.0 h1:Wt7E8J+VBCbj4FjiBfDTK/neXDDjyJVJc7xfuOHImZ0= +k8s.io/apiextensions-apiserver v0.36.0/go.mod h1:kGDjH0msuiIB3tgsYRV0kS9GqpMYMUsQ3GHv7TApyug= +k8s.io/apiextensions-apiserver v0.36.2/go.mod h1:cL1tBWe8XSaP1H30iWKGo7hf6iAUUUJPEU70dskmAnA= +k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ= +k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc= +k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4= +k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c= +k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y= +k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +sigs.k8s.io/controller-runtime v0.24.0 h1:Ck6N2LdS8Lovy1o25BB4r1xjvLEKUl1s2o9kU+KWDE4= +sigs.k8s.io/controller-runtime v0.24.0/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= +sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/file/api/go.sum.license b/file/api/go.sum.license new file mode 100644 index 000000000..be863cd5c --- /dev/null +++ b/file/api/go.sum.license @@ -0,0 +1,3 @@ +Copyright 2026 Deutsche Telekom IT GmbH + +SPDX-License-Identifier: Apache-2.0 diff --git a/file/api/v1/file_shared_types.go b/file/api/v1/file_shared_types.go new file mode 100644 index 000000000..72dcf735e --- /dev/null +++ b/file/api/v1/file_shared_types.go @@ -0,0 +1,61 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +// PublicKey is a labeled SSH public key registered on the SFTP user. +type PublicKey struct { + // Label is a human-readable identifier for the key. It must be unique per file type. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + Label string `json:"label"` + + // Key is the SSH public key value. It must be unique per file type. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + Key string `json:"key"` +} + +// Visibility defines who can see and subscribe to an exposed file type. +// +kubebuilder:validation:Enum=World;Zone;Enterprise +type Visibility string + +const ( + VisibilityWorld Visibility = "World" + VisibilityZone Visibility = "Zone" + VisibilityEnterprise Visibility = "Enterprise" +) + +func (v Visibility) String() string { + return string(v) +} + +// ApprovalStrategy defines the approval mode for subscriptions to a file type exposure. +// +kubebuilder:validation:Enum=Auto;Simple;FourEyes +type ApprovalStrategy string + +const ( + ApprovalStrategyAuto ApprovalStrategy = "Auto" + ApprovalStrategySimple ApprovalStrategy = "Simple" + ApprovalStrategyFourEyes ApprovalStrategy = "FourEyes" +) + +func (a ApprovalStrategy) String() string { + return string(a) +} + +// Approval configures how subscriptions to a file type exposure are approved. +type Approval struct { + // Strategy defines the approval mode for subscriptions to this file type. + // +kubebuilder:validation:Enum=Auto;Simple;FourEyes + // +kubebuilder:default=Simple + Strategy ApprovalStrategy `json:"strategy"` + + // TrustedTeams identifies teams that are trusted for approving subscriptions + // to this file type. Per default your own team is trusted. + // +kubebuilder:validation:Optional + // +kubebuilder:validation:MinItems=0 + // +kubebuilder:validation:MaxItems=10 + TrustedTeams []string `json:"trustedTeams,omitempty"` +} diff --git a/file/api/v1/fileexposure_types.go b/file/api/v1/fileexposure_types.go new file mode 100644 index 000000000..230dada4f --- /dev/null +++ b/file/api/v1/fileexposure_types.go @@ -0,0 +1,113 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +import ( + ctypes "github.com/telekom/controlplane/common/pkg/types" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// FileExposureSpec defines the desired state of FileExposure. +type FileExposureSpec struct { + // Approval configures how subscriptions to this file type are approved. + Approval Approval `json:"approval"` + + // Visibility defines who can see and subscribe to this file type. + // +kubebuilder:default=Enterprise + Visibility Visibility `json:"visibility,omitempty"` + + // FileType is the file type identifier this exposure belongs to. + // References the FileType CR via MakeFileTypeName() conversion. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + FileType string `json:"fileType"` + + // Sftp holds the SFTP storage-backend-specific configuration for this exposure. + // Backend-specific settings live under their own sub-object (e.g. sftp) so that + // additional storage backends can be added without polluting the spec root. + // +kubebuilder:validation:Required + Sftp SftpExposure `json:"sftp"` + + // Zone references the Zone CR where this file type is exposed. + // On this layer only the Zone ref is passed; the file domain resolves it to + // the zone-scoped service configuration for the backend. + Zone ctypes.ObjectRef `json:"zone"` +} + +// SftpExposure holds the SFTP storage-backend-specific configuration for a FileExposure. +type SftpExposure struct { + // PublicKeys are the SSH public keys registered for the provider's SFTP user. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + PublicKeys []PublicKey `json:"publicKeys"` +} + +// FileExposureStatus defines the observed state of FileExposure. +type FileExposureStatus struct { + // +listType=map + // +listMapKey=type + // +patchStrategy=merge + // +patchMergeKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` + + // Active indicates whether this exposure has been provisioned. + Active bool `json:"active,omitempty"` + + // Subscriptions references the file-domain FileSubscriptions bound to this exposure. + // +optional + Subscriptions []ctypes.ObjectRef `json:"subscriptions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="FileType",type="string",JSONPath=".spec.fileType",description="The file type identifier" +// +kubebuilder:printcolumn:name="Active",type="boolean",JSONPath=".status.active",description="Whether this exposure is provisioned" +// +kubebuilder:printcolumn:name="CreatedAt",type="date",JSONPath=".metadata.creationTimestamp",description="Creation timestamp" + +// FileExposure is the Schema for the fileexposures API. +// It declares that an application exposes a file type. The derived logical +// Application is created without an Identity client. +type FileExposure struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FileExposureSpec `json:"spec,omitempty"` + Status FileExposureStatus `json:"status,omitempty"` +} + +var _ ctypes.Object = &FileExposure{} + +func (r *FileExposure) GetConditions() []metav1.Condition { + return r.Status.Conditions +} + +func (r *FileExposure) SetCondition(condition metav1.Condition) bool { + return meta.SetStatusCondition(&r.Status.Conditions, condition) +} + +// +kubebuilder:object:root=true + +// FileExposureList contains a list of FileExposure. +type FileExposureList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []FileExposure `json:"items"` +} + +var _ ctypes.ObjectList = &FileExposureList{} + +func (r *FileExposureList) GetItems() []ctypes.Object { + items := make([]ctypes.Object, len(r.Items)) + for i := range r.Items { + items[i] = &r.Items[i] + } + return items +} + +func init() { + SchemeBuilder.Register(&FileExposure{}, &FileExposureList{}) +} diff --git a/file/api/v1/filesubscription_types.go b/file/api/v1/filesubscription_types.go new file mode 100644 index 000000000..7d2168e8e --- /dev/null +++ b/file/api/v1/filesubscription_types.go @@ -0,0 +1,97 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +import ( + ctypes "github.com/telekom/controlplane/common/pkg/types" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// FileSubscriptionSpec defines the desired state of FileSubscription. +// It is created in the file domain from a rover-domain Rover subscription (1:1). +type FileSubscriptionSpec struct { + // FileType is the file type identifier this subscription belongs to. + // References the FileType CR via MakeFileTypeName() conversion. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + FileType string `json:"fileType"` + + // Sftp holds the SFTP storage-backend-specific configuration for this subscription. + // Backend-specific settings live under their own sub-object (e.g. sftp) so that + // additional storage backends can be added without polluting the spec root. + // +kubebuilder:validation:Required + Sftp SftpSubscription `json:"sftp"` +} + +// SftpSubscription holds the SFTP storage-backend-specific configuration for a FileSubscription. +type SftpSubscription struct { + // ClientId identifies the consumer application's client on the SFTP backend. + // +optional + ClientId string `json:"clientId,omitempty"` + + // PublicKeys are the SSH public keys registered for the consumer's SFTP user. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + PublicKeys []PublicKey `json:"publicKeys"` +} + +// FileSubscriptionStatus defines the observed state of FileSubscription. +type FileSubscriptionStatus struct { + // +listType=map + // +listMapKey=type + // +patchStrategy=merge + // +patchMergeKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="FileType",type="string",JSONPath=".spec.fileType",description="The file type identifier" +// +kubebuilder:printcolumn:name="CreatedAt",type="date",JSONPath=".metadata.creationTimestamp",description="Creation timestamp" + +// FileSubscription is the Schema for the filesubscriptions API. +// It declares that an application consumes a file type. +type FileSubscription struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FileSubscriptionSpec `json:"spec,omitempty"` + Status FileSubscriptionStatus `json:"status,omitempty"` +} + +var _ ctypes.Object = &FileSubscription{} + +func (r *FileSubscription) GetConditions() []metav1.Condition { + return r.Status.Conditions +} + +func (r *FileSubscription) SetCondition(condition metav1.Condition) bool { + return meta.SetStatusCondition(&r.Status.Conditions, condition) +} + +// +kubebuilder:object:root=true + +// FileSubscriptionList contains a list of FileSubscription. +type FileSubscriptionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []FileSubscription `json:"items"` +} + +var _ ctypes.ObjectList = &FileSubscriptionList{} + +func (r *FileSubscriptionList) GetItems() []ctypes.Object { + items := make([]ctypes.Object, len(r.Items)) + for i := range r.Items { + items[i] = &r.Items[i] + } + return items +} + +func init() { + SchemeBuilder.Register(&FileSubscription{}, &FileSubscriptionList{}) +} diff --git a/file/api/v1/filetype_types.go b/file/api/v1/filetype_types.go new file mode 100644 index 000000000..1eb98386a --- /dev/null +++ b/file/api/v1/filetype_types.go @@ -0,0 +1,109 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +import ( + "strings" + + "github.com/telekom/controlplane/common/pkg/config" + ctypes "github.com/telekom/controlplane/common/pkg/types" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// FileTypeLabelKey is the label used to associate FileExposure/FileSubscription +// resources with their FileType +var FileTypeLabelKey = config.BuildLabelKey("filetype") + +// MakeFileTypeName generates a Kubernetes resource name from a file type identifier. +func MakeFileTypeName(fileType string) string { + return strings.ToLower(strings.ReplaceAll(fileType, ".", "-")) +} + +// FileTypeSpec defines the desired state of FileType. +type FileTypeSpec struct { + // Type is the dot-separated file type identifier (e.g. "de.telekom.eni.invoices.v1"). + // Used to generate the resource name via MakeFileTypeName() conversion. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^[a-z0-9]+(\.[a-z0-9]+)*$` + Type string `json:"type"` + + // Description provides a human-readable summary of this file type. + // +optional + Description string `json:"description,omitempty"` + + // Specification contains the file ID reference from the file manager for the + // optional document that describes this file type. + // +optional + Specification string `json:"specification,omitempty"` +} + +// FileTypeStatus defines the observed state of FileType. +type FileTypeStatus struct { + // +listType=map + // +listMapKey=type + // +patchStrategy=merge + // +patchMergeKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` + + // Active indicates whether this FileType is the active singleton for its file + // type identifier. When multiple FileTypes exist for the same identifier, only + // the oldest non-deleted one is active. + Active bool `json:"active,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:shortName=ftype +// +kubebuilder:printcolumn:name="Type",type="string",JSONPath=".spec.type",description="The file type identifier" +// +kubebuilder:printcolumn:name="Active",type="boolean",JSONPath=".status.active",description="Indicates if this FileType is the active singleton" +// +kubebuilder:printcolumn:name="CreatedAt",type="date",JSONPath=".metadata.creationTimestamp",description="Creation timestamp" + +// FileType is the Schema for the filetypes API. +// It represents a registered file type in the file domain, serving as the canonical +// reference that FileExposure and FileSubscription point to (mirrors event.EventType). +type FileType struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FileTypeSpec `json:"spec,omitempty"` + Status FileTypeStatus `json:"status,omitempty"` +} + +var _ ctypes.Object = &FileType{} + +func (r *FileType) GetConditions() []metav1.Condition { + return r.Status.Conditions +} + +func (r *FileType) SetCondition(condition metav1.Condition) bool { + return meta.SetStatusCondition(&r.Status.Conditions, condition) +} + +// +kubebuilder:object:root=true + +// FileTypeList contains a list of FileType. +type FileTypeList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []FileType `json:"items"` +} + +var _ ctypes.ObjectList = &FileTypeList{} + +func (r *FileTypeList) GetItems() []ctypes.Object { + items := make([]ctypes.Object, len(r.Items)) + for i := range r.Items { + items[i] = &r.Items[i] + } + return items +} + +func init() { + SchemeBuilder.Register(&FileType{}, &FileTypeList{}) +} diff --git a/file/api/v1/groupversion_info.go b/file/api/v1/groupversion_info.go new file mode 100644 index 000000000..c8295bf23 --- /dev/null +++ b/file/api/v1/groupversion_info.go @@ -0,0 +1,24 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +// Package v1 contains API Schema definitions for the file v1 API group. +// +kubebuilder:object:generate=true +// +groupName=file.ei.telekom.de +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "file.ei.telekom.de", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/file/api/v1/suite_test.go b/file/api/v1/suite_test.go new file mode 100644 index 000000000..8a6ab691d --- /dev/null +++ b/file/api/v1/suite_test.go @@ -0,0 +1,17 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package v1_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestFileApiV1(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "File API V1 Suite") +} diff --git a/file/api/v1/types_test.go b/file/api/v1/types_test.go new file mode 100644 index 000000000..8eef5c01c --- /dev/null +++ b/file/api/v1/types_test.go @@ -0,0 +1,109 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package v1_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + ctypes "github.com/telekom/controlplane/common/pkg/types" + v1 "github.com/telekom/controlplane/file/api/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("MakeFileTypeName", func() { + DescribeTable("converts file type strings to Kubernetes resource names", + func(input, expected string) { + Expect(v1.MakeFileTypeName(input)).To(Equal(expected)) + }, + Entry("file type with dots", "de.telekom.eni.foo.v1", "de-telekom-eni-foo-v1"), + Entry("already hyphenated", "de-telekom-eni-foo-v1", "de-telekom-eni-foo-v1"), + Entry("empty string", "", ""), + Entry("mixed case with dots", "De.Telekom.V1", "de-telekom-v1"), + ) +}) + +var _ = Describe("FileType", func() { + It("gets and sets conditions", func() { + ft := &v1.FileType{ + ObjectMeta: metav1.ObjectMeta{Name: "demo-v1", Namespace: "team-ns"}, + Spec: v1.FileTypeSpec{Type: "demo-v1", Description: "demo"}, + } + Expect(ft.GetConditions()).To(BeEmpty()) + changed := ft.SetCondition(metav1.Condition{ + Type: "Ready", + Status: metav1.ConditionTrue, + Reason: "Provisioned", + }) + Expect(changed).To(BeTrue()) + Expect(ft.GetConditions()).To(HaveLen(1)) + Expect(ft.GetConditions()[0].Type).To(Equal("Ready")) + }) + + It("exposes list items via GetItems", func() { + list := &v1.FileTypeList{Items: []v1.FileType{ + {ObjectMeta: metav1.ObjectMeta{Name: "a"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "b"}}, + }} + items := list.GetItems() + Expect(items).To(HaveLen(2)) + Expect(items[0].GetName()).To(Equal("a")) + Expect(items[1].GetName()).To(Equal("b")) + }) +}) + +var _ = Describe("FileExposure", func() { + It("deep-copies spec and status without aliasing", func() { + orig := &v1.FileExposure{ + ObjectMeta: metav1.ObjectMeta{Name: "foo-v1--provider", Namespace: "team-ns"}, + Spec: v1.FileExposureSpec{ + Approval: v1.Approval{Strategy: v1.ApprovalStrategySimple}, + Visibility: v1.VisibilityEnterprise, + FileType: "foo-v1", + Sftp: v1.SftpExposure{ + PublicKeys: []v1.PublicKey{ + {Label: "provider-key", Key: "ssh-ed25519 AAAA"}, + }, + }, + }, + Status: v1.FileExposureStatus{ + Active: true, + Subscriptions: []ctypes.ObjectRef{{Name: "sub", Namespace: "team-ns"}}, + }, + } + + clone := orig.DeepCopy() + Expect(clone).To(Equal(orig)) + + // Mutating the clone must not affect the original. + clone.Spec.Sftp.PublicKeys[0].Key = "changed" + clone.Status.Subscriptions[0].Name = "other" + Expect(orig.Spec.Sftp.PublicKeys[0].Key).To(Equal("ssh-ed25519 AAAA")) + Expect(orig.Status.Subscriptions[0].Name).To(Equal("sub")) + }) + + It("gets and sets conditions", func() { + exp := &v1.FileExposure{} + Expect(exp.SetCondition(metav1.Condition{Type: "Ready", Status: metav1.ConditionFalse, Reason: "Pending"})).To(BeTrue()) + Expect(exp.GetConditions()).To(HaveLen(1)) + }) +}) + +var _ = Describe("FileSubscription", func() { + It("deep-copies public keys without aliasing", func() { + orig := &v1.FileSubscription{ + Spec: v1.FileSubscriptionSpec{ + FileType: "foo-v1", + Sftp: v1.SftpSubscription{ + PublicKeys: []v1.PublicKey{{Label: "consumer-key", Key: "ssh-ed25519 BBBB"}}, + }, + }, + } + clone := orig.DeepCopy() + Expect(clone).To(Equal(orig)) + clone.Spec.Sftp.PublicKeys[0].Label = "changed" + Expect(orig.Spec.Sftp.PublicKeys[0].Label).To(Equal("consumer-key")) + }) +}) diff --git a/file/api/v1/zz_generated.deepcopy.go b/file/api/v1/zz_generated.deepcopy.go new file mode 100644 index 000000000..fc644dcbd --- /dev/null +++ b/file/api/v1/zz_generated.deepcopy.go @@ -0,0 +1,389 @@ +//go:build !ignore_autogenerated + +// SPDX-FileCopyrightText: 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +import ( + "github.com/telekom/controlplane/common/pkg/types" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Approval) DeepCopyInto(out *Approval) { + *out = *in + if in.TrustedTeams != nil { + in, out := &in.TrustedTeams, &out.TrustedTeams + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Approval. +func (in *Approval) DeepCopy() *Approval { + if in == nil { + return nil + } + out := new(Approval) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PublicKey) DeepCopyInto(out *PublicKey) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PublicKey. +func (in *PublicKey) DeepCopy() *PublicKey { + if in == nil { + return nil + } + out := new(PublicKey) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SftpExposure) DeepCopyInto(out *SftpExposure) { + *out = *in + if in.PublicKeys != nil { + in, out := &in.PublicKeys, &out.PublicKeys + *out = make([]PublicKey, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SftpExposure. +func (in *SftpExposure) DeepCopy() *SftpExposure { + if in == nil { + return nil + } + out := new(SftpExposure) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SftpSubscription) DeepCopyInto(out *SftpSubscription) { + *out = *in + if in.PublicKeys != nil { + in, out := &in.PublicKeys, &out.PublicKeys + *out = make([]PublicKey, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SftpSubscription. +func (in *SftpSubscription) DeepCopy() *SftpSubscription { + if in == nil { + return nil + } + out := new(SftpSubscription) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileTypeSpec) DeepCopyInto(out *FileTypeSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileTypeSpec. +func (in *FileTypeSpec) DeepCopy() *FileTypeSpec { + if in == nil { + return nil + } + out := new(FileTypeSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileTypeStatus) DeepCopyInto(out *FileTypeStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileTypeStatus. +func (in *FileTypeStatus) DeepCopy() *FileTypeStatus { + if in == nil { + return nil + } + out := new(FileTypeStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileType) DeepCopyInto(out *FileType) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileType. +func (in *FileType) DeepCopy() *FileType { + if in == nil { + return nil + } + out := new(FileType) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FileType) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileTypeList) DeepCopyInto(out *FileTypeList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]FileType, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileTypeList. +func (in *FileTypeList) DeepCopy() *FileTypeList { + if in == nil { + return nil + } + out := new(FileTypeList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FileTypeList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileExposureSpec) DeepCopyInto(out *FileExposureSpec) { + *out = *in + in.Approval.DeepCopyInto(&out.Approval) + in.Sftp.DeepCopyInto(&out.Sftp) + in.Zone.DeepCopyInto(&out.Zone) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileExposureSpec. +func (in *FileExposureSpec) DeepCopy() *FileExposureSpec { + if in == nil { + return nil + } + out := new(FileExposureSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileExposureStatus) DeepCopyInto(out *FileExposureStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Subscriptions != nil { + in, out := &in.Subscriptions, &out.Subscriptions + *out = make([]types.ObjectRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileExposureStatus. +func (in *FileExposureStatus) DeepCopy() *FileExposureStatus { + if in == nil { + return nil + } + out := new(FileExposureStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileExposure) DeepCopyInto(out *FileExposure) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileExposure. +func (in *FileExposure) DeepCopy() *FileExposure { + if in == nil { + return nil + } + out := new(FileExposure) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FileExposure) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileExposureList) DeepCopyInto(out *FileExposureList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]FileExposure, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileExposureList. +func (in *FileExposureList) DeepCopy() *FileExposureList { + if in == nil { + return nil + } + out := new(FileExposureList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FileExposureList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileSubscriptionSpec) DeepCopyInto(out *FileSubscriptionSpec) { + *out = *in + in.Sftp.DeepCopyInto(&out.Sftp) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSubscriptionSpec. +func (in *FileSubscriptionSpec) DeepCopy() *FileSubscriptionSpec { + if in == nil { + return nil + } + out := new(FileSubscriptionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileSubscriptionStatus) DeepCopyInto(out *FileSubscriptionStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSubscriptionStatus. +func (in *FileSubscriptionStatus) DeepCopy() *FileSubscriptionStatus { + if in == nil { + return nil + } + out := new(FileSubscriptionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileSubscription) DeepCopyInto(out *FileSubscription) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSubscription. +func (in *FileSubscription) DeepCopy() *FileSubscription { + if in == nil { + return nil + } + out := new(FileSubscription) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FileSubscription) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileSubscriptionList) DeepCopyInto(out *FileSubscriptionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]FileSubscription, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileSubscriptionList. +func (in *FileSubscriptionList) DeepCopy() *FileSubscriptionList { + if in == nil { + return nil + } + out := new(FileSubscriptionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FileSubscriptionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml b/file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml new file mode 100644 index 000000000..5badf8853 --- /dev/null +++ b/file/config/crd/bases/file.ei.telekom.de_fileexposures.yaml @@ -0,0 +1,212 @@ +# SPDX-FileCopyrightText: 2026 Deutsche Telekom IT GmbH +# +# SPDX-License-Identifier: Apache-2.0 +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: fileexposures.file.ei.telekom.de +spec: + group: file.ei.telekom.de + names: + kind: FileExposure + listKind: FileExposureList + plural: fileexposures + singular: fileexposure + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The file type identifier + jsonPath: .spec.fileType + name: FileType + type: string + - description: Whether this exposure is provisioned + jsonPath: .status.active + name: Active + type: boolean + - description: Creation timestamp + jsonPath: .metadata.creationTimestamp + name: CreatedAt + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + FileExposure is the Schema for the fileexposures API. + It declares that an application exposes a file type. The derived logical + Application is created without an Identity client. + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + description: ExposureSpec defines the desired state of a file Exposure. + properties: + approval: + description: Approval configures how subscriptions to this file type + are approved. + properties: + strategy: + default: Simple + description: Strategy defines the approval mode for subscriptions + to this file type. + enum: + - Auto + - Simple + - FourEyes + type: string + trustedTeams: + description: |- + TrustedTeams identifies teams that are trusted for approving subscriptions + to this file type. Per default your own team is trusted. + items: + type: string + maxItems: 10 + minItems: 0 + type: array + required: + - strategy + type: object + fileType: + description: |- + FileType is the file type identifier this exposure belongs to. + References the FileType CR via MakeFileTypeName() conversion. + minLength: 1 + type: string + sftp: + description: |- + Sftp holds the SFTP storage-backend-specific configuration for this exposure. + Backend-specific settings live under their own sub-object (e.g. sftp) so that + additional storage backends can be added without polluting the spec root. + properties: + publicKeys: + description: PublicKeys are the SSH public keys registered for + the provider's SFTP user. + items: + description: PublicKey is a labeled SSH public key registered + on the SFTP user. + properties: + key: + description: Key is the SSH public key value. It must be + unique per file type. + minLength: 1 + type: string + label: + description: Label is a human-readable identifier for the + key. It must be unique per file type. + minLength: 1 + type: string + required: + - key + - label + type: object + minItems: 1 + type: array + required: + - publicKeys + type: object + visibility: + default: Enterprise + description: Visibility defines who can see and subscribe to this file + type. + enum: + - World + - Zone + - Enterprise + type: string + zone: + description: |- + Zone references the Zone CR where this file type is exposed. + On this layer only the Zone ref is passed; the file domain resolves it to + the zone-scoped service configuration for the backend. + properties: + name: + type: string + namespace: + type: string + uid: + type: string + required: + - name + - namespace + type: object + required: + - approval + - fileType + - sftp + - zone + type: object + status: + description: ExposureStatus defines the observed state of a file Exposure. + properties: + active: + description: Active indicates whether this exposure has been provisioned. + type: boolean + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + subscriptions: + description: Subscriptions references the file-domain FileSubscriptions + bound to this exposure. + items: + properties: + name: + type: string + namespace: + type: string + uid: + type: string + required: + - name + - namespace + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml b/file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml new file mode 100644 index 000000000..22f793631 --- /dev/null +++ b/file/config/crd/bases/file.ei.telekom.de_filesubscriptions.yaml @@ -0,0 +1,140 @@ +# SPDX-FileCopyrightText: 2026 Deutsche Telekom IT GmbH +# +# SPDX-License-Identifier: Apache-2.0 +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: filesubscriptions.file.ei.telekom.de +spec: + group: file.ei.telekom.de + names: + kind: FileSubscription + listKind: FileSubscriptionList + plural: filesubscriptions + singular: filesubscription + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The file type identifier + jsonPath: .spec.fileType + name: FileType + type: string + - description: Creation timestamp + jsonPath: .metadata.creationTimestamp + name: CreatedAt + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + FileSubscription is the Schema for the filesubscriptions API. + It declares that an application consumes a file type. + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + description: FileSubscriptionSpec defines the desired state of FileSubscription. + properties: + fileType: + description: |- + FileType is the file type identifier this subscription belongs to. + References the FileType CR via MakeFileTypeName() conversion. + minLength: 1 + type: string + sftp: + description: |- + Sftp holds the SFTP storage-backend-specific configuration for this subscription. + Backend-specific settings live under their own sub-object (e.g. sftp) so that + additional storage backends can be added without polluting the spec root. + properties: + clientId: + description: ClientId identifies the consumer application's client + on the SFTP backend. + type: string + publicKeys: + description: PublicKeys are the SSH public keys registered for + the consumer's SFTP user. + items: + description: PublicKey is a labeled SSH public key registered + on the SFTP user. + properties: + key: + description: Key is the SSH public key value. It must be + unique per file type. + minLength: 1 + type: string + label: + description: Label is a human-readable identifier for the + key. It must be unique per file type. + minLength: 1 + type: string + required: + - key + - label + type: object + minItems: 1 + type: array + required: + - publicKeys + type: object + required: + - fileType + - sftp + type: object + status: + description: FileSubscriptionStatus defines the observed state of FileSubscription. + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/file/config/crd/bases/file.ei.telekom.de_filetypes.yaml b/file/config/crd/bases/file.ei.telekom.de_filetypes.yaml new file mode 100644 index 000000000..4bc7a03e7 --- /dev/null +++ b/file/config/crd/bases/file.ei.telekom.de_filetypes.yaml @@ -0,0 +1,128 @@ +# SPDX-FileCopyrightText: 2026 Deutsche Telekom IT GmbH +# +# SPDX-License-Identifier: Apache-2.0 +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: filetypes.file.ei.telekom.de +spec: + group: file.ei.telekom.de + names: + kind: FileType + listKind: FileTypeList + plural: filetypes + singular: filetype + shortNames: + - ftype + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The file type identifier + jsonPath: .spec.type + name: Type + type: string + - description: Indicates if this FileType is the active singleton + jsonPath: .status.active + name: Active + type: boolean + - description: Creation timestamp + jsonPath: .metadata.creationTimestamp + name: CreatedAt + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + FileType is the Schema for the filetypes API. + It represents a registered file type in the file domain, serving as the canonical + reference that FileExposure and FileSubscription point to (mirrors event.EventType). + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + description: FileTypeSpec defines the desired state of FileType. + properties: + description: + description: Description provides a human-readable summary of this + file type. + type: string + specification: + description: |- + Specification contains the file ID reference from the file manager for the + optional document that describes this file type. + type: string + type: + description: |- + Type is the dot-separated file type identifier (e.g. "de.telekom.eni.invoices.v1"). + Used to generate the resource name via MakeFileTypeName() conversion. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]+(\.[a-z0-9]+)*$ + type: string + required: + - type + type: object + status: + description: FileTypeStatus defines the observed state of FileType. + properties: + active: + description: |- + Active indicates whether this FileType is the active singleton for its file + type identifier. When multiple FileTypes exist for the same identifier, only + the oldest non-deleted one is active. + type: boolean + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + type: object + served: true + storage: true + subresources: + status: {} + diff --git a/file/config/crd/kustomization.yaml b/file/config/crd/kustomization.yaml new file mode 100644 index 000000000..2a901b41f --- /dev/null +++ b/file/config/crd/kustomization.yaml @@ -0,0 +1,13 @@ +# Copyright 2026 Deutsche Telekom IT GmbH +# +# SPDX-License-Identifier: Apache-2.0 + +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out of this kustomize package. +# It should be run by config/default +resources: +- bases/file.ei.telekom.de_filetypes.yaml +- bases/file.ei.telekom.de_fileexposures.yaml +- bases/file.ei.telekom.de_filesubscriptions.yaml +# +kubebuilder:scaffold:crdkustomizeresource +