diff --git a/examples/kustomization/base/tenant.yaml b/examples/kustomization/base/tenant.yaml index cfc8b416cfc..9173717d058 100644 --- a/examples/kustomization/base/tenant.yaml +++ b/examples/kustomization/base/tenant.yaml @@ -25,6 +25,9 @@ spec: bucketDNS: false ## Specify a list of domains used to access MinIO and Console domains: { } + ## Enable access via SFTP + ## This feature is turned off by default + # enableSFTP: false ## Create users in the Tenant using this field. Make sure to create secrets per user added here. ## Secret should follow the format used in `minio-creds-secret`. users: diff --git a/helm/operator/templates/minio.min.io_tenants.yaml b/helm/operator/templates/minio.min.io_tenants.yaml index b938e447873..f6be3c2116a 100644 --- a/helm/operator/templates/minio.min.io_tenants.yaml +++ b/helm/operator/templates/minio.min.io_tenants.yaml @@ -204,6 +204,8 @@ spec: type: string type: array type: object + enableSFTP: + type: boolean type: object image: type: string diff --git a/helm/tenant/templates/tenant.yaml b/helm/tenant/templates/tenant.yaml index be79b345c0c..de67813832b 100644 --- a/helm/tenant/templates/tenant.yaml +++ b/helm/tenant/templates/tenant.yaml @@ -91,6 +91,7 @@ spec: {{- with (dig "features" "domains" (dict) .) }} domains: {{- toYaml . | nindent 6 }} {{- end }} + enableSFTP: {{ dig "features" "enableSFTP" false . }} {{- with (dig "buckets" (list) .) }} buckets: {{- toYaml . | nindent 4 }} {{- end }} diff --git a/helm/tenant/values.yaml b/helm/tenant/values.yaml index d5d6c542dc6..b277ef80f8f 100644 --- a/helm/tenant/values.yaml +++ b/helm/tenant/values.yaml @@ -107,6 +107,7 @@ tenant: features: bucketDNS: false domains: { } + enableSFTP: false ## List of bucket definitions to create during tenant provisioning. ## Example: # - name: my-minio-bucket diff --git a/kubectl-minio/cmd/resources/tenant.go b/kubectl-minio/cmd/resources/tenant.go index fb70dea6762..d5e15e7d3c5 100644 --- a/kubectl-minio/cmd/resources/tenant.go +++ b/kubectl-minio/cmd/resources/tenant.go @@ -44,6 +44,7 @@ type TenantOptions struct { DisableAntiAffinity bool ExposeMinioService bool ExposeConsoleService bool + EnableSFTP bool Interactive bool } @@ -136,6 +137,9 @@ func NewTenant(opts *TenantOptions, userSecret *v1.Secret) (*miniov2.Tenant, err Name: userSecret.Name, }, }, + Features: &miniov2.Features{ + EnableSFTP: &opts.EnableSFTP, + }, }, } diff --git a/kubectl-minio/cmd/tenant-create.go b/kubectl-minio/cmd/tenant-create.go index 10bf032d34b..fa93ee6c3bb 100644 --- a/kubectl-minio/cmd/tenant-create.go +++ b/kubectl-minio/cmd/tenant-create.go @@ -92,6 +92,7 @@ func newTenantCreateCmd(out io.Writer, errOut io.Writer) *cobra.Command { f.BoolVar(&c.tenantOpts.Interactive, "interactive", false, "Create tenant in interactive mode") f.BoolVar(&c.tenantOpts.ExposeMinioService, "expose-minio-service", false, "Enable/Disable expose the Minio Service") f.BoolVar(&c.tenantOpts.ExposeConsoleService, "expose-console-service", false, "Enable/Disable expose the Console service") + f.BoolVar(&c.tenantOpts.EnableSFTP, "enable-sftp", false, "Enable/Disable SFTP access to the tenant") return cmd } @@ -189,6 +190,7 @@ func (c *createCmd) populateInteractiveTenant() error { c.tenantOpts.DisableTLS = helpers.Ask("Disable TLS") c.tenantOpts.ExposeMinioService = helpers.Ask("Expose Minio Service") c.tenantOpts.ExposeConsoleService = helpers.Ask("Expose Console Service") + c.tenantOpts.EnableSFTP = helpers.Ask("Enable SFTP") return nil } diff --git a/pkg/apis/minio.min.io/v2/constants.go b/pkg/apis/minio.min.io/v2/constants.go index 5bbea43083d..fc977722491 100644 --- a/pkg/apis/minio.min.io/v2/constants.go +++ b/pkg/apis/minio.min.io/v2/constants.go @@ -69,6 +69,9 @@ const Revision = "min.io/revision" // MinIOPort specifies the default Tenant port number. const MinIOPort = 9000 +// MinIOSFTPPort specifies the default Tenant SFTP port number. +const MinIOSFTPPort = 8022 + // MinIOPortLoadBalancerSVC specifies the default Service port number for the load balancer service. const MinIOPortLoadBalancerSVC = 80 @@ -81,6 +84,9 @@ const MinIOServiceHTTPPortName = "http-minio" // MinIOServiceHTTPSPortName specifies the default Service's https port name, e.g. for automatic protocol selection in Istio const MinIOServiceHTTPSPortName = "https-minio" +// MinIOServiceSFTPPortName specifies the default Service's FTP port name +const MinIOServiceSFTPPortName = "sftp-minio" + // MinIOVolumeName specifies the default volume name for MinIO volumes const MinIOVolumeName = "export" diff --git a/pkg/apis/minio.min.io/v2/types.go b/pkg/apis/minio.min.io/v2/types.go index 911f8455c22..8c6dd992632 100644 --- a/pkg/apis/minio.min.io/v2/types.go +++ b/pkg/apis/minio.min.io/v2/types.go @@ -84,6 +84,10 @@ type Features struct { // Specify a list of domains used to access MinIO and Console. // Domains *TenantDomains `json:"domains,omitempty"` + // *Optional* + + // + // Starts minio server with SFTP support + EnableSFTP *bool `json:"enableSFTP,omitempty"` } // TenantSpec (`spec`) defines the configuration of a MinIO Tenant object. + @@ -132,6 +136,7 @@ type TenantSpec struct { // If provided, the MinIO Operator adds the specified environment variables when deploying the Tenant resource. // +optional Env []corev1.EnvVar `json:"env,omitempty"` + // *Optional* + // // Enables TLS with SNI support on each MinIO pod in the tenant. If `externalCertSecret` is omitted *and* `requestAutoCert` is set to `false`, the MinIO Tenant deploys *without* TLS enabled. + diff --git a/pkg/apis/minio.min.io/v2/zz_generated.deepcopy.go b/pkg/apis/minio.min.io/v2/zz_generated.deepcopy.go index 4f8f8eaa489..a5d0b57007a 100644 --- a/pkg/apis/minio.min.io/v2/zz_generated.deepcopy.go +++ b/pkg/apis/minio.min.io/v2/zz_generated.deepcopy.go @@ -209,6 +209,11 @@ func (in *Features) DeepCopyInto(out *Features) { *out = new(TenantDomains) (*in).DeepCopyInto(*out) } + if in.EnableSFTP != nil { + in, out := &in.EnableSFTP, &out.EnableSFTP + *out = new(bool) + **out = **in + } return } @@ -748,6 +753,13 @@ func (in *TenantSpec) DeepCopyInto(out *TenantSpec) { *out = new(v1.LocalObjectReference) **out = **in } + if in.InitContainers != nil { + in, out := &in.InitContainers, &out.InitContainers + *out = make([]v1.Container, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/pkg/client/applyconfiguration/minio.min.io/v2/features.go b/pkg/client/applyconfiguration/minio.min.io/v2/features.go index 23caee16c14..4da0ab0ee9e 100644 --- a/pkg/client/applyconfiguration/minio.min.io/v2/features.go +++ b/pkg/client/applyconfiguration/minio.min.io/v2/features.go @@ -21,8 +21,9 @@ package v2 // FeaturesApplyConfiguration represents an declarative configuration of the Features type for use // with apply. type FeaturesApplyConfiguration struct { - BucketDNS *bool `json:"bucketDNS,omitempty"` - Domains *TenantDomainsApplyConfiguration `json:"domains,omitempty"` + BucketDNS *bool `json:"bucketDNS,omitempty"` + Domains *TenantDomainsApplyConfiguration `json:"domains,omitempty"` + EnableSFTP *bool `json:"enableSFTP,omitempty"` } // FeaturesApplyConfiguration constructs an declarative configuration of the Features type for use with @@ -46,3 +47,11 @@ func (b *FeaturesApplyConfiguration) WithDomains(value *TenantDomainsApplyConfig b.Domains = value return b } + +// WithEnableSFTP sets the EnableSFTP field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the EnableSFTP field is set to the value of the last call. +func (b *FeaturesApplyConfiguration) WithEnableSFTP(value bool) *FeaturesApplyConfiguration { + b.EnableSFTP = &value + return b +} diff --git a/pkg/controller/minio-services.go b/pkg/controller/minio-services.go index 4a99c59a78b..fe425c94176 100644 --- a/pkg/controller/minio-services.go +++ b/pkg/controller/minio-services.go @@ -104,6 +104,10 @@ func minioSvcMatchesSpecification(svc *v1.Service, expectedSvc *v1.Service) (boo } } // expected ports match + if len(svc.Spec.Ports) != len(expectedSvc.Spec.Ports) { + return false, errors.New("service ports don't match") + } + for i, expPort := range expectedSvc.Spec.Ports { if expPort.Name != svc.Spec.Ports[i].Name || expPort.Port != svc.Spec.Ports[i].Port || diff --git a/pkg/resources/services/service.go b/pkg/resources/services/service.go index 01b16b0bdb4..3212e6b7953 100644 --- a/pkg/resources/services/service.go +++ b/pkg/resources/services/service.go @@ -150,6 +150,13 @@ func ServiceForBucket(t *miniov2.Tenant, bucket string) *corev1.Service { // NewHeadlessForMinIO will return a new headless Kubernetes service for a Tenant func NewHeadlessForMinIO(t *miniov2.Tenant) *corev1.Service { minioPort := corev1.ServicePort{Port: miniov2.MinIOPort, Name: miniov2.MinIOServiceHTTPPortName} + ports := []corev1.ServicePort{minioPort} + + if t.Spec.Features != nil && t.Spec.Features.EnableSFTP != nil && *t.Spec.Features.EnableSFTP { + minioSFTPPort := corev1.ServicePort{Port: miniov2.MinIOSFTPPort, Name: miniov2.MinIOServiceSFTPPortName} + ports = append(ports, minioSFTPPort) + } + svc := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Labels: t.MinIOPodLabels(), @@ -158,7 +165,7 @@ func NewHeadlessForMinIO(t *miniov2.Tenant) *corev1.Service { OwnerReferences: t.OwnerRef(), }, Spec: corev1.ServiceSpec{ - Ports: []corev1.ServicePort{minioPort}, + Ports: ports, Selector: t.MinIOPodLabels(), Type: corev1.ServiceTypeClusterIP, ClusterIP: corev1.ClusterIPNone, diff --git a/pkg/resources/statefulsets/minio-statefulset.go b/pkg/resources/statefulsets/minio-statefulset.go index 6b21c6148b9..840e295fc52 100644 --- a/pkg/resources/statefulsets/minio-statefulset.go +++ b/pkg/resources/statefulsets/minio-statefulset.go @@ -16,10 +16,12 @@ package statefulsets import ( "fmt" + "path/filepath" "sort" "strconv" "strings" + "github.com/minio/operator/pkg/certs" "github.com/minio/operator/pkg/common" miniov2 "github.com/minio/operator/pkg/apis/minio.min.io/v2" @@ -292,7 +294,32 @@ func poolMinioServerContainer(t *miniov2.Tenant, skipEnvVars map[string][]byte, if t.TLS() { consolePort = miniov2.ConsoleTLSPort } - args := []string{"server", "--certs-dir", miniov2.MinIOCertPath, "--console-address", ":" + strconv.Itoa(consolePort)} + args := []string{ + "server", + "--certs-dir", miniov2.MinIOCertPath, + "--console-address", ":" + strconv.Itoa(consolePort), + } + + containerPorts := []corev1.ContainerPort{ + { + ContainerPort: miniov2.MinIOPort, + }, + { + ContainerPort: int32(consolePort), + }, + } + + if t.Spec.Features != nil && t.Spec.Features.EnableSFTP != nil && *t.Spec.Features.EnableSFTP { + pkFile := filepath.Join(miniov2.MinIOCertPath, certs.PrivateKeyFile) + args = append(args, []string{ + "--sftp", fmt.Sprintf("address=:%d", miniov2.MinIOSFTPPort), + "--sftp", "ssh-private-key=" + pkFile, + }...) + containerPorts = append(containerPorts, v1.ContainerPort{ + ContainerPort: miniov2.MinIOSFTPPort, + }) + } + if t.Spec.Logging != nil { // If logging is specified, expect users to // provide the right set of settings to toggle @@ -309,16 +336,9 @@ func poolMinioServerContainer(t *miniov2.Tenant, skipEnvVars map[string][]byte, } return corev1.Container{ - Name: miniov2.MinIOServerName, - Image: t.Spec.Image, - Ports: []corev1.ContainerPort{ - { - ContainerPort: miniov2.MinIOPort, - }, - { - ContainerPort: int32(consolePort), - }, - }, + Name: miniov2.MinIOServerName, + Image: t.Spec.Image, + Ports: containerPorts, ImagePullPolicy: t.Spec.ImagePullPolicy, VolumeMounts: volumeMounts(t, pool, certVolumeSources), Args: args, diff --git a/resources/base/crds/minio.min.io_tenants.yaml b/resources/base/crds/minio.min.io_tenants.yaml index 985d53fcd96..d6cd62bcc58 100644 --- a/resources/base/crds/minio.min.io_tenants.yaml +++ b/resources/base/crds/minio.min.io_tenants.yaml @@ -205,6 +205,8 @@ spec: type: string type: array type: object + enableSFTP: + type: boolean type: object image: type: string