diff --git a/common/pkg/config/feature.go b/common/pkg/config/feature.go index 9d72ca9f0..3fe3da163 100644 --- a/common/pkg/config/feature.go +++ b/common/pkg/config/feature.go @@ -49,6 +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 + FeatureFile Feature = NewFeature("file", true) ) // SetFeatureEnabled sets the enabled state for a feature. Intended for tests. 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/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 + diff --git a/rover-ctl/pkg/handlers/v0/rover.go b/rover-ctl/pkg/handlers/v0/rover.go index a3d50bbb0..527abaa5c 100644 --- a/rover-ctl/pkg/handlers/v0/rover.go +++ b/rover-ctl/pkg/handlers/v0/rover.go @@ -121,7 +121,9 @@ 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 { @@ -150,7 +152,9 @@ 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 7045b572d..5b1daac39 100644 --- a/rover-ctl/pkg/handlers/v0/rover_test.go +++ b/rover-ctl/pkg/handlers/v0/rover_test.go @@ -226,6 +226,34 @@ var _ = Describe("Rover Handler", func() { Expect(exposures).To(HaveLen(1)) Expect(exposures[0]).To(HaveKeyWithValue("type", "ai")) }) + + 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() { @@ -343,6 +371,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-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. - - diff --git a/rover-server/api/openapi.yaml b/rover-server/api/openapi.yaml index 43483463a..acba41813 100644 --- a/rover-server/api/openapi.yaml +++ b/rover-server/api/openapi.yaml @@ -303,7 +303,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 @@ -735,7 +735,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 @@ -971,7 +971,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 @@ -1993,6 +1993,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: + - sftp + description: File-transfer backend. Optional; currently only "sftp" 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: @@ -2001,10 +2035,12 @@ components: api: '#/components/schemas/ApiExposure' event: '#/components/schemas/EventExposure' ai: '#/components/schemas/AiExposure' + file: '#/components/schemas/FileExposure' oneOf: - $ref: '#/components/schemas/ApiExposure' - $ref: '#/components/schemas/EventExposure' - $ref: '#/components/schemas/AiExposure' + - $ref: '#/components/schemas/FileExposure' ApiExposureInfo: @@ -2034,6 +2070,15 @@ components: type: string enum: - ai + FileExposureInfo: + type: object + allOf: + - $ref: '#/components/schemas/FileExposure' + - properties: + type: + type: string + enum: + - file ExposureInfo: discriminator: propertyName: type @@ -2041,10 +2086,12 @@ components: api: '#/components/schemas/ApiExposureInfo' event: '#/components/schemas/EventExposureInfo' ai: '#/components/schemas/AiExposureInfo' + file: '#/components/schemas/FileExposureInfo' oneOf: - $ref: '#/components/schemas/ApiExposureInfo' - $ref: '#/components/schemas/EventExposureInfo' - $ref: '#/components/schemas/AiExposureInfo' + - $ref: '#/components/schemas/FileExposureInfo' ApplicationInfo: type: object @@ -2324,7 +2371,7 @@ components: - id allOf: - $ref: '#/components/schemas/RateLimit' - - properties: + - properties: id: type: string description: The unique ID of this consumer (their clientId) @@ -2727,6 +2774,21 @@ components: $ref: '#/components/schemas/Failover' security: $ref: '#/components/schemas/Security' + FileSubscription: + type: object + required: + - type + - fileType + - publicKeys + properties: + type: + type: string + fileType: + type: string + publicKeys: + type: array + items: + $ref: '#/components/schemas/PublicKey' Subscription: type: object discriminator: @@ -2735,10 +2797,12 @@ components: api: '#/components/schemas/ApiSubscription' event: '#/components/schemas/EventSubscription' ai: '#/components/schemas/AiSubscription' + file: '#/components/schemas/FileSubscription' oneOf: - $ref: '#/components/schemas/ApiSubscription' - $ref: '#/components/schemas/EventSubscription' - $ref: '#/components/schemas/AiSubscription' + - $ref: '#/components/schemas/FileSubscription' ApiSubscriptionInfo: type: object @@ -2775,6 +2839,15 @@ components: type: string enum: - ai + FileSubscriptionInfo: + type: object + allOf: + - $ref: '#/components/schemas/FileSubscription' + - properties: + type: + type: string + enum: + - file SubscriptionInfo: readOnly: true discriminator: @@ -2783,10 +2856,12 @@ components: api: '#/components/schemas/ApiSubscriptionInfo' event: '#/components/schemas/EventSubscriptionInfo' ai: '#/components/schemas/AiSubscriptionInfo' + file: '#/components/schemas/FileSubscriptionInfo' oneOf: - $ref: '#/components/schemas/ApiSubscriptionInfo' - $ref: '#/components/schemas/EventSubscriptionInfo' - $ref: '#/components/schemas/AiSubscriptionInfo' + - $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 f8262c36c..3bc848d8a 100644 --- a/rover-server/internal/api/server.gen.go +++ b/rover-server/internal/api/server.gen.go @@ -261,6 +261,66 @@ func (e EventTriggerResponseFilterMode) Valid() bool { } } +// Defines values for FileExposureVariant. +const ( + FileExposureVariantSftp FileExposureVariant = "sftp" +) + +// Valid indicates whether the value is a known member of the FileExposureVariant enum. +func (e FileExposureVariant) Valid() bool { + switch e { + case FileExposureVariantSftp: + return true + default: + return false + } +} + +// 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 ( + FileExposureInfoVariantSftp FileExposureInfoVariant = "sftp" +) + +// Valid indicates whether the value is a known member of the FileExposureInfoVariant enum. +func (e FileExposureInfoVariant) Valid() bool { + switch e { + case FileExposureInfoVariantSftp: + return true + default: + return false + } +} + +// 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 GrantType. const ( CLIENTCREDENTIALS GrantType = "CLIENT_CREDENTIALS" @@ -1145,6 +1205,54 @@ 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 "sftp" is supported. + Variant FileExposureVariant `json:"variant,omitempty,omitzero"` + Visibility Visibility `json:"visibility,omitempty,omitzero"` +} + +// FileExposureVariant File-transfer backend. Optional; currently only "sftp" 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 "sftp" 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 "sftp" is supported. +type FileExposureInfoVariant string + +// FileSubscription defines model for FileSubscription. +type FileSubscription struct { + FileType string `json:"fileType"` + PublicKeys []PublicKey `json:"publicKeys"` + Type string `json:"type"` +} + +// FileSubscriptionInfo defines model for FileSubscriptionInfo. +type FileSubscriptionInfo struct { + FileType string `json:"fileType"` + PublicKeys []PublicKey `json:"publicKeys"` + Type FileSubscriptionInfoType `json:"type"` +} + +// FileSubscriptionInfoType defines model for FileSubscriptionInfo.Type. +type FileSubscriptionInfoType string + // GrantType defines model for GrantType. type GrantType string @@ -1244,6 +1352,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"` @@ -1746,6 +1863,34 @@ func (t *Exposure) MergeAiExposure(v AiExposure) 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"` @@ -1766,6 +1911,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) } @@ -1865,6 +2012,34 @@ func (t *ExposureInfo) MergeAiExposureInfo(v AiExposureInfo) 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"` @@ -1885,6 +2060,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) } @@ -2073,6 +2250,34 @@ func (t *Subscription) MergeAiSubscription(v AiSubscription) 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"` @@ -2093,6 +2298,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) } @@ -2192,6 +2399,34 @@ func (t *SubscriptionInfo) MergeAiSubscriptionInfo(v AiSubscriptionInfo) error { 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"` @@ -2212,6 +2447,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) } @@ -2230,162 +2467,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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", } // GetSwagger returns the content of the embedded swagger specification file 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 ed8538ab9..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,6 +47,7 @@ }, Event: (*v1.EventExposure)(nil), Agentic: (*v1.AgenticExposure)(nil), + File: (*v1.FileExposure)(nil), } --- @@ -64,9 +65,26 @@ AdditionalPublisherIds: nil, }, Agentic: (*v1.AgenticExposure)(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), + Agentic: (*v1.AgenticExposure)(nil), + File: &v1.FileExposure{ + FileType: "demo-sftp-spec-v1", + 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 04a4b7b51..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,6 +26,7 @@ }, Event: (*v1.EventExposure)(nil), Agentic: (*v1.AgenticExposure)(nil), + File: (*v1.FileExposure)(nil), }, }, Subscriptions: { @@ -39,6 +40,7 @@ }, Event: (*v1.EventSubscription)(nil), Agentic: (*v1.AgenticSubscription)(nil), + File: (*v1.FileSubscription)(nil), }, }, Permissions: nil, @@ -75,6 +77,7 @@ }, Event: (*v1.EventExposure)(nil), Agentic: (*v1.AgenticExposure)(nil), + File: (*v1.FileExposure)(nil), }, }, Subscriptions: nil, @@ -106,6 +109,7 @@ }, Event: (*v1.EventSubscription)(nil), Agentic: (*v1.AgenticSubscription)(nil), + File: (*v1.FileSubscription)(nil), }, }, Permissions: nil, @@ -158,6 +162,7 @@ }, Event: (*v1.EventExposure)(nil), Agentic: (*v1.AgenticExposure)(nil), + File: (*v1.FileExposure)(nil), }, }, Subscriptions: { @@ -171,6 +176,7 @@ }, Event: (*v1.EventSubscription)(nil), Agentic: (*v1.AgenticSubscription)(nil), + File: (*v1.FileSubscription)(nil), }, }, Permissions: nil, @@ -248,6 +254,7 @@ }, Event: (*v1.EventExposure)(nil), Agentic: (*v1.AgenticExposure)(nil), + File: (*v1.FileExposure)(nil), }, }, Subscriptions: { @@ -261,6 +268,7 @@ }, Event: (*v1.EventSubscription)(nil), Agentic: (*v1.AgenticSubscription)(nil), + File: (*v1.FileSubscription)(nil), }, }, Permissions: nil, @@ -313,6 +321,7 @@ }, Event: (*v1.EventExposure)(nil), Agentic: (*v1.AgenticExposure)(nil), + File: (*v1.FileExposure)(nil), }, }, Subscriptions: { @@ -326,6 +335,7 @@ }, Event: (*v1.EventSubscription)(nil), Agentic: (*v1.AgenticSubscription)(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 25f971bc3..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,6 +24,7 @@ }, Event: (*v1.EventSubscription)(nil), Agentic: (*v1.AgenticSubscription)(nil), + File: (*v1.FileSubscription)(nil), } --- @@ -37,9 +38,24 @@ Scopes: nil, }, Agentic: (*v1.AgenticSubscription)(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), + Agentic: (*v1.AgenticSubscription)(nil), + File: &v1.FileSubscription{ + FileType: "demo-sftp-spec-v1", + 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 e7db799e1..245af177d 100644 --- a/rover-server/internal/mapper/rover/in/exposure.go +++ b/rover-server/internal/mapper/rover/in/exposure.go @@ -62,6 +62,14 @@ func mapExposure(in *api.Exposure, out *roverv1.Exposure) error { out.Agentic = mapAiExposure(aiExp) + 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) } @@ -69,6 +77,30 @@ func mapExposure(in *api.Exposure, out *roverv1.Exposure) error { return nil } +func mapFileExposure(in api.FileExposure) *roverv1.FileExposure { + out := &roverv1.FileExposure{ + FileType: in.FileType, + 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 555e07d94..89ddd9c2f 100644 --- a/rover-server/internal/mapper/rover/in/exposure_test.go +++ b/rover-server/internal/mapper/rover/in/exposure_test.go @@ -97,6 +97,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/file_test.go b/rover-server/internal/mapper/rover/in/file_test.go new file mode 100644 index 000000000..94233db43 --- /dev/null +++ b/rover-server/internal/mapper/rover/in/file_test.go @@ -0,0 +1,168 @@ +// 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", + 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.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 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", + PublicKeys: []api.PublicKey{ + {Label: "provider-key", Key: "ssh-ed25519 AAAA1"}, + }, + } + + output := mapFileExposure(input) + + Expect(output.Visibility).To(Equal(roverv1.VisibilityEnterprise)) + }) + + 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() { + It("must map a FileSubscription correctly", func() { + input := api.FileSubscription{ + Type: "file", + FileType: "demo-sftp-spec-v1", + 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.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-server/internal/mapper/rover/in/subscription.go b/rover-server/internal/mapper/rover/in/subscription.go index 7ac2ed243..8f57d9225 100644 --- a/rover-server/internal/mapper/rover/in/subscription.go +++ b/rover-server/internal/mapper/rover/in/subscription.go @@ -91,6 +91,14 @@ func mapSubscription(in *api.Subscription, out *roverv1.Subscription) error { out.Agentic = mapAiSubscription(aiSub) + 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) @@ -99,6 +107,13 @@ func mapSubscription(in *api.Subscription, out *roverv1.Subscription) error { return nil } +func mapFileSubscription(in api.FileSubscription) *roverv1.FileSubscription { + return &roverv1.FileSubscription{ + FileType: in.FileType, + 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 2c0b34721..d2246aa44 100644 --- a/rover-server/internal/mapper/rover/in/subscription_test.go +++ b/rover-server/internal/mapper/rover/in/subscription_test.go @@ -89,6 +89,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..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 @@ -40,6 +40,23 @@ var ( EventType: "test-event", } + fileExposure = api.FileExposure{ + Type: "file", + FileType: "demo-sftp-spec-v1", + Visibility: "World", + PublicKeys: []api.PublicKey{ + {Label: "provider-key", Key: "ssh-ed25519 AAAA-provider"}, + }, + } + + fileSubscription = api.FileSubscription{ + Type: "file", + FileType: "demo-sftp-spec-v1", + PublicKeys: []api.PublicKey{ + {Label: "consumer-key", Key: "ssh-ed25519 AAAA-consumer"}, + }, + } + resourceIdInfo = mapper.ResourceIdInfo{ Name: "rover-local-sub", Environment: "poc", @@ -93,3 +110,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 +} diff --git a/rover-server/internal/mapper/rover/out/exposure.go b/rover-server/internal/mapper/rover/out/exposure.go index ff3e795ea..a46246415 100644 --- a/rover-server/internal/mapper/rover/out/exposure.go +++ b/rover-server/internal/mapper/rover/out/exposure.go @@ -52,12 +52,39 @@ func mapExposure(in *roverv1.Exposure, out *api.Exposure) error { return errors.Wrap(err, "failed to map ai 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, + 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, error) { apiExposure := api.ApiExposure{ BasePath: in.BasePath, 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/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/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/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), + ) + }) +}) diff --git a/rover/api/v1/rover_types.go b/rover/api/v1/rover_types.go index bb27b724b..d34f5f3e7 100644 --- a/rover/api/v1/rover_types.go +++ b/rover/api/v1/rover_types.go @@ -32,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"` // AgenticExposures are references to AgenticExposure resources created by this Rover @@ -214,6 +226,8 @@ const ( TypeEvent Type = "event" // TypeAgentic represents an Agentic type resource (MCP, A2A) TypeAgentic Type = "agentic" + // TypeFile represents a File type resource (SFTP integration) + TypeFile Type = "file" ) // ApprovalStrategy defines the approval workflow for API exposure @@ -277,6 +291,9 @@ type Exposure struct { // Agentic defines an Agentic(MCP or agent) server exposure configuration // +kubebuilder:validation:Optional Agentic *AgenticExposure `json:"agentic,omitempty"` + // File defines a File-based (SFTP) service exposure configuration + // +kubebuilder:validation:Optional + File *FileExposure `json:"file,omitempty"` } func (e *Exposure) Type() Type { @@ -289,6 +306,9 @@ func (e *Exposure) Type() Type { if e.Agentic != nil { return TypeAgentic } + if e.File != nil { + return TypeFile + } return "" } @@ -305,6 +325,9 @@ type Subscription struct { // Agentic defines an Agentic(MCP or agent) server subscription configuration // +kubebuilder:validation:Optional Agentic *AgenticSubscription `json:"agentic,omitempty"` + // File defines a File-based (SFTP) service subscription configuration + // +kubebuilder:validation:Optional + File *FileSubscription `json:"file,omitempty"` } func (s *Subscription) Type() Type { @@ -317,6 +340,9 @@ func (s *Subscription) Type() Type { if s.Agentic != nil { return TypeAgentic } + if s.File != nil { + return TypeFile + } return "" } @@ -510,6 +536,90 @@ type AgenticSubscription struct { Security *SubscriberSecurity `json:"security,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"` + + // Visibility defines who can see and subscribe to this file type + // +kubebuilder:validation:Enum=World;Zone;Enterprise + // +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 + // +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"` + + // 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"` +} + +// 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/api/v1/rover_types_test.go b/rover/api/v1/rover_types_test.go index 35b0855e8..86186345f 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,119 @@ 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 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", + 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 0c1d2e97d..bef303bd4 100644 --- a/rover/api/v1/zz_generated.deepcopy.go +++ b/rover/api/v1/zz_generated.deepcopy.go @@ -782,6 +782,11 @@ func (in *Exposure) DeepCopyInto(out *Exposure) { *out = new(AgenticExposure) (*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. @@ -834,6 +839,144 @@ 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 + in.Approval.DeepCopyInto(&out.Approval) + 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 *HeaderTransformation) DeepCopyInto(out *HeaderTransformation) { *out = *in @@ -1148,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 @@ -1524,6 +1682,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)) @@ -1680,6 +1852,11 @@ func (in *Subscription) DeepCopyInto(out *Subscription) { *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/cmd/main.go b/rover/cmd/main.go index cac8c1f11..8e900c780 100644 --- a/rover/cmd/main.go +++ b/rover/cmd/main.go @@ -10,6 +10,12 @@ import ( "fmt" "os" + filev1 "github.com/telekom/controlplane/file/api/v1" + + // 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" + "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -35,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 ( @@ -66,6 +68,10 @@ func init() { utilruntime.Must(agenticv1.AddToScheme(scheme)) } // +kubebuilder:scaffold:scheme + if cconfig.FeatureFile.IsEnabled() { + utilruntime.Must(filev1.AddToScheme(scheme)) + } + // +kubebuilder:scaffold:scheme } func main() { @@ -200,6 +206,16 @@ func main() { os.Exit(1) } + 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..8f7d89251 --- /dev/null +++ b/rover/config/crd/bases/rover.cp.ei.telekom.de_filespecifications.yaml @@ -0,0 +1,156 @@ +# SPDX-FileCopyrightText: 2025 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. + 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 + 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. + 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 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 + 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 7d71290fc..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 @@ -1113,6 +1113,96 @@ spec: - eventType - visibility type: object + file: + 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 + 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 + visibility: + default: Enterprise + description: Visibility defines who can see and subscribe + to this file type + enum: + - World + - Zone + - Enterprise + type: string + required: + - approval + - fileType + - publicKeys + - visibility + type: object type: object maxItems: 150 type: array @@ -1652,6 +1742,44 @@ 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 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 + required: + - fileType + - publicKeys + type: object type: object maxItems: 150 type: array @@ -1882,6 +2010,58 @@ spec: - namespace type: object type: array + fileExposures: + 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 + 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. + + (rover/internal/handler/rover/handler.go, case roverv1.TypeFile) once delivered. + 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 48fb16cb9..9687e5a8e 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 - bases/rover.cp.ei.telekom.de_mcpspecifications.yaml diff --git a/rover/config/rbac/role.yaml b/rover/config/rbac/role.yaml index 528dd419f..ec872ffe1 100644 --- a/rover/config/rbac/role.yaml +++ b/rover/config/rbac/role.yaml @@ -92,6 +92,20 @@ rules: - patch - update - watch +- apiGroups: + - file.cp.ei.telekom.de + resources: + - fileexposures + - filesubscriptions + - filetypes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - organization.cp.ei.telekom.de resources: @@ -118,6 +132,7 @@ rules: - apichangelogs - apispecifications - eventspecifications + - filespecifications - mcpspecifications - roadmaps - rovers @@ -135,6 +150,7 @@ rules: - apichangelogs/finalizers - apispecifications/finalizers - eventspecifications/finalizers + - filespecifications/finalizers - mcpspecifications/finalizers - roadmaps/finalizers - rovers/finalizers @@ -146,6 +162,7 @@ rules: - apichangelogs/status - apispecifications/status - eventspecifications/status + - filespecifications/status - mcpspecifications/status - roadmaps/status - rovers/status 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 73a1199f9..a0678baeb 100644 --- a/rover/go.mod +++ b/rover/go.mod @@ -26,6 +26,8 @@ require ( github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.11.1 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 @@ -41,6 +43,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/go.sum b/rover/go.sum index c3588b1e6..5f2e2dcf8 100644 --- a/rover/go.sum +++ b/rover/go.sum @@ -209,6 +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.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/controller/filespecification_controller.go b/rover/internal/controller/filespecification_controller.go new file mode 100644 index 000000000..5c7062983 --- /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 + +//nolint:dupl // Single-resource controller scaffolds are intentionally kept parallel for clarity. +package controller + +import ( + "context" + + "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" + + 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 +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.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{}) +} + +// 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/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/rover_controller.go b/rover/internal/controller/rover_controller.go index 716555848..a1e794a46 100644 --- a/rover/internal/controller/rover_controller.go +++ b/rover/internal/controller/rover_controller.go @@ -24,6 +24,7 @@ import ( cconfig "github.com/telekom/controlplane/common/pkg/config" cc "github.com/telekom/controlplane/common/pkg/controller" 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" @@ -57,6 +58,9 @@ 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.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 // +kubebuilder:rbac:groups=application.cp.ei.telekom.de,resources=applications,verbs=get;list;watch;create;update;patch;delete @@ -90,6 +94,11 @@ func (r *RoverReconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(&agenticv1.AgenticSubscription{}) } + 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/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/filespecification/handler.go b/rover/internal/handler/filespecification/handler.go new file mode 100644 index 000000000..9abbc7596 --- /dev/null +++ b/rover/internal/handler/filespecification/handler.go @@ -0,0 +1,79 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package filespecification + +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" + "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" +) + +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/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()) + }) +}) diff --git a/rover/internal/handler/rover/application/application.go b/rover/internal/handler/rover/application/application.go index 215e11cff..588fafcd7 100644 --- a/rover/internal/handler/rover/application/application.go +++ b/rover/internal/handler/rover/application/application.go @@ -46,13 +46,7 @@ func HandleApplication(ctx context.Context, c client.JanitorClient, owner *rover 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 := isClientNeeded(owner) var hasAnySubscriptionFailoverEnabled bool if needsClient { @@ -131,3 +125,17 @@ func HandleApplication(ctx context.Context, c client.JanitorClient, owner *rover return err } + +// 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 + }) + 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/application/application_test.go b/rover/internal/handler/rover/application/application_test.go new file mode 100644 index 000000000..121e7cab8 --- /dev/null +++ b/rover/internal/handler/rover/application/application_test.go @@ -0,0 +1,56 @@ +// Copyright 2025 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package application + +import ( + "testing" + + 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) { + 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(isClientNeeded(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), + ) +}) diff --git a/rover/internal/handler/rover/file/exposure.go b/rover/internal/handler/rover/file/exposure.go new file mode 100644 index 000000000..58cf9e800 --- /dev/null +++ b/rover/internal/handler/rover/file/exposure.go @@ -0,0 +1,76 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package file + +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/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" +) + +// 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 { + logger := log.FromContext(ctx) + logger.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, + }, + } + + environment := contextutil.EnvFromContextOrDie(ctx) + zoneRef := types.ObjectRef{ + Name: owner.Spec.Zone, + Namespace: environment, + } + + 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(zoneRef.Name), + config.BuildLabelKey("application"): labelutil.NormalizeLabelValue(owner.Name), + } + + fileExposure.Spec = filev1.FileExposureSpec{ + Approval: filev1.Approval{Strategy: filev1.ApprovalStrategy(exp.Approval.Strategy)}, + Visibility: filev1.Visibility(exp.Visibility.String()), + FileType: exp.FileType, + Sftp: filev1.SftpExposure{ + PublicKeys: mapPublicKeys(exp.PublicKeys), + }, + Zone: zoneRef, + } + 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/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)) + }) + }) +}) diff --git a/rover/internal/handler/rover/file/subscription.go b/rover/internal/handler/rover/file/subscription.go new file mode 100644 index 000000000..ff5ab431f --- /dev/null +++ b/rover/internal/handler/rover/file/subscription.go @@ -0,0 +1,66 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package file + +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" +) + +// 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 { + logger := log.FromContext(ctx) + logger.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, + }, + } + + 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{ + FileType: sub.FileType, + Sftp: filev1.SftpSubscription{ + 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..59ff060c0 --- /dev/null +++ b/rover/internal/handler/rover/file/util.go @@ -0,0 +1,29 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +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" +) + +// 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) + "--" + labelutil.NormalizeValue(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..1bf1c9526 --- /dev/null +++ b/rover/internal/handler/rover/file/util_test.go @@ -0,0 +1,47 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package file + +import ( + roverv1 "github.com/telekom/controlplane/rover/api/v1" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +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"), + ) +}) + +var _ = Describe("mapPublicKeys", func() { + It("yields nil for nil input", func() { + Expect(mapPublicKeys(nil)).To(BeNil()) + }) + + It("yields nil for an empty slice", func() { + Expect(mapPublicKeys([]roverv1.PublicKey{})).To(BeNil()) + }) + + 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) + 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")) + }) +}) diff --git a/rover/internal/handler/rover/handler.go b/rover/internal/handler/rover/handler.go index a8fd739f5..f88ff969b 100644 --- a/rover/internal/handler/rover/handler.go +++ b/rover/internal/handler/rover/handler.go @@ -21,12 +21,14 @@ 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/agentic" "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" ) @@ -76,6 +78,10 @@ func addKnownTypes(c client.JanitorClient) { if config.FeaturePermission.IsEnabled() { c.AddKnownTypeToState(&permissionv1.PermissionSet{}) } + if config.FeatureFile.IsEnabled() { + c.AddKnownTypeToState(&filev1.FileExposure{}) + c.AddKnownTypeToState(&filev1.FileSubscription{}) + } if config.FeatureAiGateway.IsEnabled() { c.AddKnownTypeToState(&agenticv1.AgenticExposure{}) c.AddKnownTypeToState(&agenticv1.AgenticSubscription{}) @@ -86,6 +92,7 @@ func (h *RoverHandler) handleExposures(ctx context.Context, c client.JanitorClie roverObj.Status.ApiExposures = make([]types.ObjectRef, 0, len(roverObj.Spec.Exposures)) roverObj.Status.EventExposures = make([]types.ObjectRef, 0, len(roverObj.Spec.Exposures)) roverObj.Status.AgenticExposures = make([]types.ObjectRef, 0, len(roverObj.Spec.Exposures)) + roverObj.Status.FileExposures = make([]types.ObjectRef, 0, len(roverObj.Spec.Exposures)) seenDiscriminators := make(map[string]struct{}) for _, exp := range roverObj.Spec.Exposures { @@ -128,10 +135,19 @@ func (h *RoverHandler) handleExposure(ctx context.Context, c client.JanitorClien if err := agentic.HandleExposure(ctx, c, roverObj, exp.Agentic); err != nil { 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() { + 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 } @@ -140,6 +156,7 @@ func (h *RoverHandler) handleSubscriptions(ctx context.Context, c client.Janitor roverObj.Status.EventSubscriptions = make([]types.ObjectRef, 0, len(roverObj.Spec.Subscriptions)) roverObj.Status.AgenticSubscriptions = 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 { if err := h.handleSubscription(ctx, c, roverObj, sub, logger); err != nil { return err @@ -171,10 +188,19 @@ func (h *RoverHandler) handleSubscription(ctx context.Context, c client.JanitorC if err := agentic.HandleSubscription(ctx, c, roverObj, sub.Agentic); err != nil { return errors.Wrap(err, "failed to handle AI subscription") } + + 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 } diff --git a/rover/internal/webhook/v1/filespecification_webhook.go b/rover/internal/webhook/v1/filespecification_webhook.go new file mode 100644 index 000000000..e24aabd55 --- /dev/null +++ b/rover/internal/webhook/v1/filespecification_webhook.go @@ -0,0 +1,71 @@ +// 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, _, 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) + + // 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("storageType"), + string(st), + fmt.Sprintf("spec.storageType must be %q", roverv1.FileStorageTypeSFTP), + ) + } + + 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..0351a7147 --- /dev/null +++ b/rover/internal/webhook/v1/filespecification_webhook_test.go @@ -0,0 +1,275 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package v1 + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "strings" + + "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)) + } + + 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: newED25519Key()}, + {Label: "consumer-key", Key: newED25519Key()}, + } + 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: newED25519Key()}, + {Label: "dup", Key: newED25519Key()}, + } + 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() + sameKey := newED25519Key() + keys := []roverv1.PublicKey{ + {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() { + 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("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: newED25519Key()}}, + }} + } + + 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("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 + + BeforeEach(func() { + validator = &FileSpecificationCustomValidator{client: k8sClient} + }) + + newFileSpec := func(name string, storageType roverv1.FileStorageType) *roverv1.FileSpecification { + return &roverv1.FileSpecification{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "default"}, + Spec: roverv1.FileSpecificationSpec{Description: "demo", StorageType: storageType}, + } + } + + 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 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.storageType must be")) + }) + }) +}) + +// 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 ec81ac1e7..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" @@ -297,6 +298,8 @@ func (r *RoverValidator) ValidateExposure(ctx context.Context, valErr *cerrors.V return r.ValidateEventExposure(ctx, valErr, environment, exposure, zoneRef, idx) case roverv1.TypeAgentic: return r.ValidateAiExposure(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"), @@ -423,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 @@ -463,6 +468,16 @@ func MustNotHaveDuplicates(valErr *cerrors.ValidationError, subs []roverv1.Subsc fmt.Sprintf("duplicate subscription for agentic base path %s", sub.Agentic.BasePath), ) } + + 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) @@ -493,6 +508,16 @@ func MustNotHaveDuplicates(valErr *cerrors.ValidationError, subs []roverv1.Subsc fmt.Sprintf("duplicate exposure for agentic base path %s", exposure.Agentic.BasePath), ) } + + 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 @@ -741,7 +766,85 @@ func (r *RoverValidator) ValidateSubscription(ctx context.Context, valErr *cerro return nil case roverv1.TypeAgentic: return nil // AI subscriptions have no special validation at this time + + 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{}{} + + 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, + ), + ) + } +} 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 ) 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=