diff --git a/.openshift-ci/tests/e2e.sh b/.openshift-ci/tests/e2e.sh index 87ab7df089..2202c91f48 100755 --- a/.openshift-ci/tests/e2e.sh +++ b/.openshift-ci/tests/e2e.sh @@ -23,6 +23,7 @@ if [[ "${OPENSHIFT_CI:-}" == "true" ]]; then export "${secret_name}"="${secret_value}" done export STATIC_TOKEN="${FLEET_STATIC_TOKEN:-}" + export STATIC_TOKEN_ADMIN="${FLEET_STATIC_TOKEN_ADMIN:-}" export QUAY_USER="${IMAGE_PUSH_USERNAME:-}" export QUAY_TOKEN="${IMAGE_PUSH_PASSWORD:-}" export CLUSTER_TYPE="openshift-ci" diff --git a/.secrets.baseline b/.secrets.baseline index e717b6e77c..738a1ea9e8 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -493,7 +493,7 @@ "filename": "openapi/fleet-manager-private.yaml", "hashed_secret": "2774b5ad0fae1c8b8dc897b89db85529d45cb5cf", "is_verified": false, - "line_number": 472, + "line_number": 474, "is_secret": false } ], @@ -503,7 +503,7 @@ "filename": "openapi/fleet-manager.yaml", "hashed_secret": "a5f0056c7d0ca4ed78e2f3b551554daaf4721934", "is_verified": false, - "line_number": 1037, + "line_number": 1094, "is_secret": false } ], @@ -834,5 +834,5 @@ } ] }, - "generated_at": "2022-08-05T13:45:51Z" + "generated_at": "2022-08-09T08:36:37Z" } diff --git a/dev/env/manifests/shared/03-configmap-config.yaml b/dev/env/manifests/shared/03-configmap-config.yaml index 228a5db594..4fd1f666a7 100644 --- a/dev/env/manifests/shared/03-configmap-config.yaml +++ b/dev/env/manifests/shared/03-configmap-config.yaml @@ -204,6 +204,11 @@ data: any_user: true max_allowed_instances: 3 registered_users: [] + # Static token's org_id, see config/static-token-payload.json + - id: 12345678 + any_user: true + max_allowed_instances: 100 + registered_users: [] read-only-user-list.yaml: |- --- # A list of users with read-only permissions for data plane clusters @@ -227,6 +232,11 @@ data: roles: - "acs-general-engineering" # Will include all of ACS engineering. Available also within staging environment. - "acs-fleet-manager-admin-full" # Prod rover group, will only include selected members + SREs. + - method: POST + roles: + - "acs-general-engineering" # Will include all of ACS engineering. Available also within staging environment. + - "acs-fleet-manager-admin-full" # Prod rover group, will only include selected members + SREs. + - "acs-fleet-manager-admin-write" # Prod rover group, will only include selected members + SREs. admin-authz-roles-prod.yaml: |- --- - method: GET @@ -241,6 +251,10 @@ data: - method: DELETE roles: - "acs-fleet-manager-admin-full" # Prod rover group, will only include selected members + SREs. + - method: POST + roles: + - "acs-fleet-manager-admin-full" # Prod rover group, will only include selected members + SREs. + - "acs-fleet-manager-admin-write" # Prod rover group, will only include selected members + SREs. kind: ConfigMap metadata: name: config diff --git a/e2e/admin_client_api.go b/e2e/admin_client_api.go new file mode 100644 index 0000000000..ab89ac23f7 --- /dev/null +++ b/e2e/admin_client_api.go @@ -0,0 +1,111 @@ +package e2e + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/golang/glog" + "github.com/pkg/errors" + "github.com/stackrox/acs-fleet-manager/fleetshard/pkg/fleetmanager" + "github.com/stackrox/acs-fleet-manager/internal/dinosaur/compat" + "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/public" +) + +// Client represents the REST client for connecting to fleet-manager +type Client struct { + client http.Client + auth fleetmanager.Auth + adminAPIEndpoint string +} + +func (c *Client) newRequest(method string, url string, body io.Reader) (*http.Response, error) { + glog.Infof("Send request to %s", url) + r, err := http.NewRequest(method, url, body) + if err != nil { + return nil, fmt.Errorf("building HTTP request: %w", err) + } + if err := c.auth.AddAuth(r); err != nil { + return nil, fmt.Errorf("adding authentication information to HTTP request: %w", err) + } + + resp, err := c.client.Do(r) + if err != nil { + return nil, fmt.Errorf("executing HTTP request: %w", err) + } + return resp, nil +} + +// unmarshalResponse unmarshalls a fleet-manager response. It returns an error if +// fleet-manager returns errors from its API. +// If the value v is nil the response is not marshalled into a struct, instead only checked for an API error. +func (c *Client) unmarshalResponse(resp *http.Response, v interface{}) error { + defer func() { _ = resp.Body.Close() }() + data, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("reading HTTP response body: %w", err) + } + if len(data) == 0 { + return nil + } + + into := struct { + Kind string `json:"kind"` + }{} + err = json.Unmarshal(data, &into) + if err != nil { + return fmt.Errorf("extracting kind information from HTTP response: %w", err) + } + + // Unmarshal error + if into.Kind == "Error" || into.Kind == "error" { + apiError := compat.Error{} + err = json.Unmarshal(data, &apiError) + if err != nil { + return fmt.Errorf("unmarshalling HTTP response as error: %w", err) + } + return errors.Errorf("API error (HTTP status %d) occured %s: %s", resp.StatusCode, apiError.Code, apiError.Reason) + } + + if v == nil { + return nil + } + + err = json.Unmarshal(data, v) + if err != nil { + return fmt.Errorf("unmarshalling HTTP response as %T: %w", v, err) + } + + return nil +} + +// NewAdminClient ... +func NewAdminClient(uriBase string, auth fleetmanager.Auth) (*Client, error) { + return &Client{ + client: http.Client{}, + auth: auth, + adminAPIEndpoint: fmt.Sprintf("%s/%s", uriBase, "api/rhacs/v1/admin"), + }, nil +} + +// CreateCentral creates a central from the public fleet-manager API +func (c *Client) CreateCentral(request public.CentralRequestPayload) (*public.CentralRequest, error) { + reqBody, err := json.Marshal(request) + if err != nil { + return nil, fmt.Errorf("marshalling HTTP request: %w", err) + } + + resp, err := c.newRequest(http.MethodPost, fmt.Sprintf("%s/dinosaurs?async=true", c.adminAPIEndpoint), bytes.NewBuffer(reqBody)) + if err != nil { + return nil, fmt.Errorf("executing HTTP request: %w", err) + } + + result := &public.CentralRequest{} + err = c.unmarshalResponse(resp, result) + if err != nil { + return nil, fmt.Errorf("unmarshalling HTTP response: %w", err) + } + return result, nil +} diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index 54e1bf3330..a4fdf45f51 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -2,6 +2,8 @@ package e2e import ( "context" + "crypto/rand" + "encoding/hex" "fmt" "os" "strings" @@ -11,9 +13,11 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" openshiftRouteV1 "github.com/openshift/api/route/v1" + "github.com/stackrox/acs-fleet-manager/e2e/envtokenauth" "github.com/stackrox/acs-fleet-manager/fleetshard/pkg/fleetmanager" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/constants" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/public" + "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/converters" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/services" "github.com/stackrox/rox/operator/apis/platform/v1alpha1" v1 "k8s.io/api/core/v1" @@ -22,9 +26,17 @@ import ( ) // TODO(ROX-11468): Why is a central always created as a "eval" instance type? -var ( - centralName = fmt.Sprintf("%s-%d", "e2e-test-central", time.Now().UnixMilli()) -) +func newCentralName() string { + rnd := make([]byte, 8) + _, err := rand.Read(rnd) + + if err != nil { + panic(fmt.Errorf("reading random bytes for unique central name: %w", err)) + } + rndString := hex.EncodeToString(rnd) + + return fmt.Sprintf("%s-%s", "e2e", rndString) +} const ( defaultPolling = 1 * time.Second @@ -35,26 +47,37 @@ const ( // TODO(ROX-11465): Use correct OCM_TOKEN for different clients (console.redhat.com, fleetshard) var _ = Describe("Central", func() { var client *fleetmanager.Client + var adminClient *Client BeforeEach(func() { authType := "OCM" if val := os.Getenv("AUTH_TYPE"); val != "" { authType = val } + GinkgoWriter.Printf("AUTH_TYPE=%q\n", authType) + fleetManagerEndpoint := "http://localhost:8000" if fmEndpointEnv := os.Getenv("FLEET_MANAGER_ENDPOINT"); fmEndpointEnv != "" { fleetManagerEndpoint = fmEndpointEnv } + GinkgoWriter.Printf("FLEET_MANAGER_ENDPOINT=%q\n", fleetManagerEndpoint) auth, err := fleetmanager.NewAuth(authType) Expect(err).ToNot(HaveOccurred()) client, err = fleetmanager.NewClient(fleetManagerEndpoint, "cluster-id", auth) Expect(err).ToNot(HaveOccurred()) + + adminAuth, err := envtokenauth.CreateAuth("STATIC_TOKEN_ADMIN") + Expect(err).ToNot(HaveOccurred()) + adminClient, err = NewAdminClient(fleetManagerEndpoint, adminAuth) + Expect(err).ToNot(HaveOccurred()) + }) Describe("should be created and deployed to k8s", func() { var err error + centralName := newCentralName() request := public.CentralRequestPayload{ Name: centralName, MultiAz: true, @@ -220,6 +243,120 @@ var _ = Describe("Central", func() { }).WithTimeout(waitTimeout).WithPolling(defaultPolling).Should(Equal(0)) }) }) + + Describe("should be created and deployed to k8s with admin API", func() { + var err error + centralName := newCentralName() + + centralResources := public.ResourceRequirements{ + Requests: public.ResourceList{ + Cpu: "501m", Memory: "201M", + }, + Limits: public.ResourceList{ + Cpu: "502m", Memory: "202M", + }, + } + centralSpec := public.CentralSpec{ + Resources: centralResources, + } + scannerResources := public.ResourceRequirements{ + Requests: public.ResourceList{ + Cpu: "301m", Memory: "151M", + }, + Limits: public.ResourceList{ + Cpu: "302m", Memory: "152M", + }, + } + scannerScaling := public.ScannerSpecAnalyzerScaling{ + AutoScaling: "Enabled", + Replicas: 1, + MinReplicas: 1, + MaxReplicas: 2, + } + scannerSpec := public.ScannerSpec{ + Analyzer: public.ScannerSpecAnalyzer{ + Resources: scannerResources, + Scaling: scannerScaling, + }, + } + request := public.CentralRequestPayload{ + Name: centralName, + MultiAz: true, + CloudProvider: dpCloudProvider, + Region: dpRegion, + Central: centralSpec, + Scanner: scannerSpec, + } + + var createdCentral *public.CentralRequest + var namespaceName string + It("should create central with custom resource configuration", func() { + createdCentral, err = adminClient.CreateCentral(request) + Expect(err).To(BeNil()) + namespaceName, err = services.FormatNamespace(createdCentral.Id) + Expect(err).To(BeNil()) + Expect(constants.DinosaurRequestStatusAccepted.String()).To(Equal(createdCentral.Status)) + }) + + central := &v1alpha1.Central{} + It("should create central in its namespace on a managed cluster", func() { + Eventually(func() error { + return k8sClient.Get(context.Background(), ctrlClient.ObjectKey{Name: centralName, Namespace: namespaceName}, central) + }).WithTimeout(waitTimeout).WithPolling(defaultPolling).Should(Succeed()) + }) + + It("central resources match configured settings", func() { + coreV1Resources := central.Spec.Central.DeploymentSpec.Resources + expectedResources, err := converters.ConvertPublicResourceRequirementsToCoreV1(¢ralResources) + Expect(err).ToNot(HaveOccurred()) + Expect(*coreV1Resources).To(Equal(expectedResources)) + }) + + It("scanner analyzer resources match configured settings", func() { + coreV1Resources := central.Spec.Scanner.Analyzer.DeploymentSpec.Resources + expectedResources, err := converters.ConvertPublicResourceRequirementsToCoreV1(&scannerResources) + Expect(err).ToNot(HaveOccurred()) + Expect(*coreV1Resources).To(Equal(expectedResources)) + + scaling := central.Spec.Scanner.Analyzer.Scaling + expectedScaling, err := converters.ConvertPublicScalingToV1(&scannerScaling) + Expect(err).ToNot(HaveOccurred()) + Expect(*scaling).To(Equal(expectedScaling)) + }) + + It("should transition central's state to ready", func() { + Eventually(func() string { + return centralStatus(createdCentral, client) + }).WithTimeout(waitTimeout).WithPolling(defaultPolling).Should(Equal(constants.DinosaurRequestStatusReady.String())) + }) + + It("should transition central to deprovisioning state", func() { + err = client.DeleteCentral(createdCentral.Id) + Expect(err).To(Succeed()) + Eventually(func() string { + deprovisioningCentral, err := client.GetCentral(createdCentral.Id) + Expect(err).To(BeNil()) + return deprovisioningCentral.Status + }).WithTimeout(waitTimeout).WithPolling(defaultPolling).Should(Equal(constants.DinosaurRequestStatusDeprovision.String())) + }) + + It("should delete central CR", func() { + Eventually(func() bool { + central := &v1alpha1.Central{} + err := k8sClient.Get(context.Background(), ctrlClient.ObjectKey{Name: centralName, Namespace: centralName}, central) + return apiErrors.IsNotFound(err) + }).WithTimeout(waitTimeout).WithPolling(defaultPolling).Should(BeTrue()) + }) + + It("should remove central namespace", func() { + Eventually(func() bool { + ns := &v1.Namespace{} + err := k8sClient.Get(context.Background(), ctrlClient.ObjectKey{Name: namespaceName}, ns) + return apiErrors.IsNotFound(err) + }).WithTimeout(waitTimeout).WithPolling(defaultPolling).Should(BeTrue()) + }) + + }) }) func getCentral(createdCentral *public.CentralRequest, client *fleetmanager.Client) *public.CentralRequest { diff --git a/e2e/envtokenauth/auth_env_token.go b/e2e/envtokenauth/auth_env_token.go new file mode 100644 index 0000000000..c6d7332193 --- /dev/null +++ b/e2e/envtokenauth/auth_env_token.go @@ -0,0 +1,33 @@ +package envtokenauth + +import ( + "fmt" + "net/http" + "os" + + "github.com/stackrox/acs-fleet-manager/fleetshard/pkg/fleetmanager" +) + +// Implements the Auth interface for simple static token based authentication +// while fetching the token from a custom environment variable. +type envTokenAuth struct { + token string +} + +// CreateAuth creates a new Auth instance which implements static token authentication +// while fetching the token from the environment using the specified environment variable name. +func CreateAuth(name string) (fleetmanager.Auth, error) { + token := os.Getenv(name) + if token == "" { + return nil, fmt.Errorf("no token named %q found in current environment", name) + } + return &envTokenAuth{ + token: token, + }, nil +} + +// AddAuth adds an Authorization header to the provided HTTP request. +func (a *envTokenAuth) AddAuth(req *http.Request) error { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", a.token)) + return nil +} diff --git a/fleetshard/pkg/central/reconciler/reconciler.go b/fleetshard/pkg/central/reconciler/reconciler.go index 0bf5ff9735..d1cf618466 100644 --- a/fleetshard/pkg/central/reconciler/reconciler.go +++ b/fleetshard/pkg/central/reconciler/reconciler.go @@ -15,6 +15,7 @@ import ( "github.com/stackrox/acs-fleet-manager/fleetshard/pkg/util" centralConstants "github.com/stackrox/acs-fleet-manager/internal/dinosaur/constants" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/private" + "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/converters" "github.com/stackrox/rox/operator/apis/platform/v1alpha1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -70,6 +71,20 @@ func (r *CentralReconciler) Reconcile(ctx context.Context, remoteCentral private centralMonitoringExposeEndpointEnabled := v1alpha1.ExposeEndpointEnabled + centralResources, err := converters.ConvertPrivateResourceRequirementsToCoreV1(&remoteCentral.Spec.Central.Resources) + if err != nil { + return nil, errors.Wrap(err, "converting Central resources") + } + scannerAnalyzerResources, err := converters.ConvertPrivateResourceRequirementsToCoreV1(&remoteCentral.Spec.Scanner.Analyzer.Resources) + if err != nil { + return nil, errors.Wrap(err, "converting Scanner Analyzer resources") + } + scannerAnalyzerScaling := converters.ConvertPrivateScalingToV1(&remoteCentral.Spec.Scanner.Analyzer.Scaling) + scannerDbResources, err := converters.ConvertPrivateResourceRequirementsToCoreV1(&remoteCentral.Spec.Scanner.Db.Resources) + if err != nil { + return nil, errors.Wrap(err, "converting Scanner DB resources") + } + central := &v1alpha1.Central{ ObjectMeta: metav1.ObjectMeta{ Name: remoteCentralName, @@ -86,6 +101,20 @@ func (r *CentralReconciler) Reconcile(ctx context.Context, remoteCentral private Monitoring: &v1alpha1.Monitoring{ ExposeEndpoint: ¢ralMonitoringExposeEndpointEnabled, }, + DeploymentSpec: v1alpha1.DeploymentSpec{ + Resources: ¢ralResources, + }, + }, + Scanner: &v1alpha1.ScannerComponentSpec{ + Analyzer: &v1alpha1.ScannerAnalyzerComponent{ + DeploymentSpec: v1alpha1.DeploymentSpec{ + Resources: &scannerAnalyzerResources, + }, + Scaling: &scannerAnalyzerScaling, + }, + DB: &v1alpha1.DeploymentSpec{ + Resources: &scannerDbResources, + }, }, }, } diff --git a/internal/dinosaur/pkg/api/dbapi/central_request_types.go b/internal/dinosaur/pkg/api/dbapi/central_request_types.go index 3ab4cd7074..4673400e65 100644 --- a/internal/dinosaur/pkg/api/dbapi/central_request_types.go +++ b/internal/dinosaur/pkg/api/dbapi/central_request_types.go @@ -27,7 +27,9 @@ type CentralRequest struct { OrganisationID string `json:"organisation_id" gorm:"index"` FailedReason string `json:"failed_reason"` // PlacementID field should be updated every time when a CentralRequest is assigned to an OSD cluster (even if it's the same one again) - PlacementID string `json:"placement_id"` + PlacementID string `json:"placement_id"` + Central api.JSON `json:"central"` // Schema is defined by dbapi.CentralSpec + Scanner api.JSON `json:"scanner"` // Schema is defined by dbapi.ScannerSpec DesiredCentralVersion string `json:"desired_central_version"` ActualCentralVersion string `json:"actual_central_version"` diff --git a/internal/dinosaur/pkg/api/dbapi/central_spec.go b/internal/dinosaur/pkg/api/dbapi/central_spec.go new file mode 100644 index 0000000000..b1dfa775ba --- /dev/null +++ b/internal/dinosaur/pkg/api/dbapi/central_spec.go @@ -0,0 +1,33 @@ +package dbapi + +import corev1 "k8s.io/api/core/v1" + +// CentralSpec ... +type CentralSpec struct { + Resources corev1.ResourceRequirements `json:"resources"` +} + +// ScannerAnalyzerScaling ... +type ScannerAnalyzerScaling struct { + AutoScaling string `json:"autoScaling,omitempty"` + Replicas int32 `json:"replicas,omitempty"` + MinReplicas int32 `json:"minReplicas,omitempty"` + MaxReplicas int32 `json:"maxReplicas,omitempty"` +} + +// ScannerAnalyzerSpec ... +type ScannerAnalyzerSpec struct { + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + Scaling ScannerAnalyzerScaling `json:"scaling,omitempty"` +} + +// ScannerDbSpec ... +type ScannerDbSpec struct { + Resources corev1.ResourceRequirements `json:"resources,omitempty"` +} + +// ScannerSpec ... +type ScannerSpec struct { + Analyzer ScannerAnalyzerSpec `json:"analyzer,omitempty"` + Db ScannerDbSpec `json:"db,omitempty"` +} diff --git a/internal/dinosaur/pkg/api/private/api/openapi.yaml b/internal/dinosaur/pkg/api/private/api/openapi.yaml index 2081f14c7a..c822b75328 100644 --- a/internal/dinosaur/pkg/api/private/api/openapi.yaml +++ b/internal/dinosaur/pkg/api/private/api/openapi.yaml @@ -482,6 +482,8 @@ components: properties: host: type: string + resources: + $ref: '#/components/schemas/ResourceRequirements' ManagedCentral_allOf_spec_scanner: properties: analyzer: diff --git a/internal/dinosaur/pkg/api/private/model_managed_central_all_of_spec_scanner_db.go b/internal/dinosaur/pkg/api/private/model_managed_central_all_of_spec_scanner_db.go index 017a148706..b9a7118e59 100644 --- a/internal/dinosaur/pkg/api/private/model_managed_central_all_of_spec_scanner_db.go +++ b/internal/dinosaur/pkg/api/private/model_managed_central_all_of_spec_scanner_db.go @@ -11,5 +11,6 @@ package private // ManagedCentralAllOfSpecScannerDb struct for ManagedCentralAllOfSpecScannerDb type ManagedCentralAllOfSpecScannerDb struct { - Host string `json:"host,omitempty"` + Host string `json:"host,omitempty"` + Resources ResourceRequirements `json:"resources,omitempty"` } diff --git a/internal/dinosaur/pkg/api/public/api/openapi.yaml b/internal/dinosaur/pkg/api/public/api/openapi.yaml index d755cec224..003b4e27a0 100644 --- a/internal/dinosaur/pkg/api/public/api/openapi.yaml +++ b/internal/dinosaur/pkg/api/public/api/openapi.yaml @@ -1220,6 +1220,30 @@ components: allOf: - $ref: '#/components/schemas/List' - $ref: '#/components/schemas/ErrorList_allOf' + ResourceList: + example: + memory: memory + cpu: cpu + properties: + cpu: + type: string + memory: + type: string + type: object + ResourceRequirements: + example: + requests: + memory: memory + cpu: cpu + limits: + memory: memory + cpu: cpu + properties: + requests: + $ref: '#/components/schemas/ResourceList' + limits: + $ref: '#/components/schemas/ResourceList' + type: object CentralRequest: allOf: - $ref: '#/components/schemas/ObjectReference' @@ -1228,6 +1252,48 @@ components: allOf: - $ref: '#/components/schemas/List' - $ref: '#/components/schemas/CentralRequestList_allOf' + CentralSpec: + example: + resources: + requests: + memory: memory + cpu: cpu + limits: + memory: memory + cpu: cpu + properties: + resources: + $ref: '#/components/schemas/ResourceRequirements' + type: object + ScannerSpec: + example: + analyzer: + scaling: + maxReplicas: 1 + autoScaling: autoScaling + minReplicas: 1 + replicas: 1 + resources: + requests: + memory: memory + cpu: cpu + limits: + memory: memory + cpu: cpu + db: + resources: + requests: + memory: memory + cpu: cpu + limits: + memory: memory + cpu: cpu + properties: + analyzer: + $ref: '#/components/schemas/ScannerSpec_analyzer' + db: + $ref: '#/components/schemas/ScannerSpec_db' + type: object VersionMetadata: allOf: - $ref: '#/components/schemas/ObjectReference' @@ -1244,8 +1310,38 @@ components: CentralRequestPayload: description: Schema for the request body sent to /centrals POST example: + central: + resources: + requests: + memory: memory + cpu: cpu + limits: + memory: memory + cpu: cpu multi_az: true name: name + scanner: + analyzer: + scaling: + maxReplicas: 1 + autoScaling: autoScaling + minReplicas: 1 + replicas: 1 + resources: + requests: + memory: memory + cpu: cpu + limits: + memory: memory + cpu: cpu + db: + resources: + requests: + memory: memory + cpu: cpu + limits: + memory: memory + cpu: cpu cloud_provider: cloud_provider region: region properties: @@ -1265,6 +1361,10 @@ components: description: The region where the Central component cluster will be created in type: string + central: + $ref: '#/components/schemas/CentralSpec' + scanner: + $ref: '#/components/schemas/ScannerSpec' required: - name type: object @@ -1416,6 +1516,10 @@ components: type: string instance_type: type: string + central: + $ref: '#/components/schemas/CentralSpec' + scanner: + $ref: '#/components/schemas/ScannerSpec' required: - multi_az CentralRequestList_allOf: @@ -1426,6 +1530,58 @@ components: allOf: - $ref: '#/components/schemas/CentralRequest' type: array + ScannerSpec_analyzer_scaling: + example: + maxReplicas: 1 + autoScaling: autoScaling + minReplicas: 1 + replicas: 1 + properties: + autoScaling: + type: string + replicas: + format: int32 + minimum: 1 + type: integer + minReplicas: + format: int32 + minimum: 1 + type: integer + maxReplicas: + format: int32 + minimum: 1 + type: integer + ScannerSpec_analyzer: + example: + scaling: + maxReplicas: 1 + autoScaling: autoScaling + minReplicas: 1 + replicas: 1 + resources: + requests: + memory: memory + cpu: cpu + limits: + memory: memory + cpu: cpu + properties: + resources: + $ref: '#/components/schemas/ResourceRequirements' + scaling: + $ref: '#/components/schemas/ScannerSpec_analyzer_scaling' + ScannerSpec_db: + example: + resources: + requests: + memory: memory + cpu: cpu + limits: + memory: memory + cpu: cpu + properties: + resources: + $ref: '#/components/schemas/ResourceRequirements' VersionMetadata_allOf: example: '{"kind":"APIVersion","id":"v1","href":"/api/rhacs/v1","collections":[{"id":"centrals","href":"/api/rhacs/v1/centrals","kind":"CentralList"}]}' properties: diff --git a/internal/dinosaur/pkg/api/public/model_central_request.go b/internal/dinosaur/pkg/api/public/model_central_request.go index 1f76b571ee..047d7d8304 100644 --- a/internal/dinosaur/pkg/api/public/model_central_request.go +++ b/internal/dinosaur/pkg/api/public/model_central_request.go @@ -24,13 +24,15 @@ type CentralRequest struct { CloudProvider string `json:"cloud_provider,omitempty"` MultiAz bool `json:"multi_az"` // Values will be regions of specific cloud provider. For example: us-east-1 for AWS - Region string `json:"region,omitempty"` - Owner string `json:"owner,omitempty"` - Name string `json:"name,omitempty"` - Host string `json:"host,omitempty"` - CreatedAt time.Time `json:"created_at,omitempty"` - UpdatedAt time.Time `json:"updated_at,omitempty"` - FailedReason string `json:"failed_reason,omitempty"` - Version string `json:"version,omitempty"` - InstanceType string `json:"instance_type,omitempty"` + Region string `json:"region,omitempty"` + Owner string `json:"owner,omitempty"` + Name string `json:"name,omitempty"` + Host string `json:"host,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` + FailedReason string `json:"failed_reason,omitempty"` + Version string `json:"version,omitempty"` + InstanceType string `json:"instance_type,omitempty"` + Central CentralSpec `json:"central,omitempty"` + Scanner ScannerSpec `json:"scanner,omitempty"` } diff --git a/internal/dinosaur/pkg/api/public/model_central_request_payload.go b/internal/dinosaur/pkg/api/public/model_central_request_payload.go index 09a4024650..438f47e099 100644 --- a/internal/dinosaur/pkg/api/public/model_central_request_payload.go +++ b/internal/dinosaur/pkg/api/public/model_central_request_payload.go @@ -18,5 +18,7 @@ type CentralRequestPayload struct { // The name of the Central component. It must consist of lower-case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character, and can not be longer than 32 characters. Name string `json:"name"` // The region where the Central component cluster will be created in - Region string `json:"region,omitempty"` + Region string `json:"region,omitempty"` + Central CentralSpec `json:"central,omitempty"` + Scanner ScannerSpec `json:"scanner,omitempty"` } diff --git a/internal/dinosaur/pkg/api/public/model_central_spec.go b/internal/dinosaur/pkg/api/public/model_central_spec.go new file mode 100644 index 0000000000..739055da23 --- /dev/null +++ b/internal/dinosaur/pkg/api/public/model_central_spec.go @@ -0,0 +1,15 @@ +/* + * Red Hat Advanced Cluster Security Service Fleet Manager + * + * Red Hat Advanced Cluster Security (RHACS) Service Fleet Manager is a Rest API to manage instances of ACS components. + * + * API version: 1.2.0 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package public + +// CentralSpec struct for CentralSpec +type CentralSpec struct { + Resources ResourceRequirements `json:"resources,omitempty"` +} diff --git a/internal/dinosaur/pkg/api/public/model_resource_list.go b/internal/dinosaur/pkg/api/public/model_resource_list.go new file mode 100644 index 0000000000..9eacca40bb --- /dev/null +++ b/internal/dinosaur/pkg/api/public/model_resource_list.go @@ -0,0 +1,16 @@ +/* + * Red Hat Advanced Cluster Security Service Fleet Manager + * + * Red Hat Advanced Cluster Security (RHACS) Service Fleet Manager is a Rest API to manage instances of ACS components. + * + * API version: 1.2.0 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package public + +// ResourceList struct for ResourceList +type ResourceList struct { + Cpu string `json:"cpu,omitempty"` + Memory string `json:"memory,omitempty"` +} diff --git a/internal/dinosaur/pkg/api/public/model_resource_requirements.go b/internal/dinosaur/pkg/api/public/model_resource_requirements.go new file mode 100644 index 0000000000..94e283f8a4 --- /dev/null +++ b/internal/dinosaur/pkg/api/public/model_resource_requirements.go @@ -0,0 +1,16 @@ +/* + * Red Hat Advanced Cluster Security Service Fleet Manager + * + * Red Hat Advanced Cluster Security (RHACS) Service Fleet Manager is a Rest API to manage instances of ACS components. + * + * API version: 1.2.0 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package public + +// ResourceRequirements struct for ResourceRequirements +type ResourceRequirements struct { + Requests ResourceList `json:"requests,omitempty"` + Limits ResourceList `json:"limits,omitempty"` +} diff --git a/internal/dinosaur/pkg/api/public/model_scanner_spec.go b/internal/dinosaur/pkg/api/public/model_scanner_spec.go new file mode 100644 index 0000000000..4b4e5a6bdf --- /dev/null +++ b/internal/dinosaur/pkg/api/public/model_scanner_spec.go @@ -0,0 +1,16 @@ +/* + * Red Hat Advanced Cluster Security Service Fleet Manager + * + * Red Hat Advanced Cluster Security (RHACS) Service Fleet Manager is a Rest API to manage instances of ACS components. + * + * API version: 1.2.0 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package public + +// ScannerSpec struct for ScannerSpec +type ScannerSpec struct { + Analyzer ScannerSpecAnalyzer `json:"analyzer,omitempty"` + Db ScannerSpecDb `json:"db,omitempty"` +} diff --git a/internal/dinosaur/pkg/api/public/model_scanner_spec_analyzer.go b/internal/dinosaur/pkg/api/public/model_scanner_spec_analyzer.go new file mode 100644 index 0000000000..77cb62c39d --- /dev/null +++ b/internal/dinosaur/pkg/api/public/model_scanner_spec_analyzer.go @@ -0,0 +1,16 @@ +/* + * Red Hat Advanced Cluster Security Service Fleet Manager + * + * Red Hat Advanced Cluster Security (RHACS) Service Fleet Manager is a Rest API to manage instances of ACS components. + * + * API version: 1.2.0 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package public + +// ScannerSpecAnalyzer struct for ScannerSpecAnalyzer +type ScannerSpecAnalyzer struct { + Resources ResourceRequirements `json:"resources,omitempty"` + Scaling ScannerSpecAnalyzerScaling `json:"scaling,omitempty"` +} diff --git a/internal/dinosaur/pkg/api/public/model_scanner_spec_analyzer_scaling.go b/internal/dinosaur/pkg/api/public/model_scanner_spec_analyzer_scaling.go new file mode 100644 index 0000000000..566c14d588 --- /dev/null +++ b/internal/dinosaur/pkg/api/public/model_scanner_spec_analyzer_scaling.go @@ -0,0 +1,18 @@ +/* + * Red Hat Advanced Cluster Security Service Fleet Manager + * + * Red Hat Advanced Cluster Security (RHACS) Service Fleet Manager is a Rest API to manage instances of ACS components. + * + * API version: 1.2.0 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package public + +// ScannerSpecAnalyzerScaling struct for ScannerSpecAnalyzerScaling +type ScannerSpecAnalyzerScaling struct { + AutoScaling string `json:"autoScaling,omitempty"` + Replicas int32 `json:"replicas,omitempty"` + MinReplicas int32 `json:"minReplicas,omitempty"` + MaxReplicas int32 `json:"maxReplicas,omitempty"` +} diff --git a/internal/dinosaur/pkg/api/public/model_scanner_spec_db.go b/internal/dinosaur/pkg/api/public/model_scanner_spec_db.go new file mode 100644 index 0000000000..011e7ccc1a --- /dev/null +++ b/internal/dinosaur/pkg/api/public/model_scanner_spec_db.go @@ -0,0 +1,15 @@ +/* + * Red Hat Advanced Cluster Security Service Fleet Manager + * + * Red Hat Advanced Cluster Security (RHACS) Service Fleet Manager is a Rest API to manage instances of ACS components. + * + * API version: 1.2.0 + * Generated by: OpenAPI Generator (https://openapi-generator.tech) + */ + +package public + +// ScannerSpecDb struct for ScannerSpecDb +type ScannerSpecDb struct { + Resources ResourceRequirements `json:"resources,omitempty"` +} diff --git a/internal/dinosaur/pkg/config/dataplane_cluster_config.go b/internal/dinosaur/pkg/config/dataplane_cluster_config.go index 2d0d883d86..9d74c6c027 100644 --- a/internal/dinosaur/pkg/config/dataplane_cluster_config.go +++ b/internal/dinosaur/pkg/config/dataplane_cluster_config.go @@ -254,6 +254,36 @@ func (c *DataplaneClusterConfig) IsReadyDataPlaneClustersReconcileEnabled() bool return c.EnableReadyDataPlaneClustersReconcile } +type stringValue string + +func (s *stringValue) Set(val string) error { + *s = stringValue(val) + return nil +} +func (s *stringValue) Type() string { + return "string" +} + +func (s *stringValue) String() string { return string(*s) } + +func (c *DataplaneClusterConfig) addKubeconfigFlag(fs *pflag.FlagSet) { + name := "kubeconfig" + usage := "A path to kubeconfig file used for communication with standalone clusters" + if kubeconfigFlag := fs.Lookup(name); kubeconfigFlag != nil { + // controller-runtime has already added a "kubeconfig" flag. We make sure that its definition + // aligns with our expectations. + *kubeconfigFlag = pflag.Flag{ + Name: name, + Shorthand: "", + Usage: usage, + Value: (*stringValue)(&c.Kubeconfig), + DefValue: c.Kubeconfig, + } + } else { + fs.StringVar(&c.Kubeconfig, name, c.Kubeconfig, usage) + } +} + // AddFlags ... func (c *DataplaneClusterConfig) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&c.OpenshiftVersion, "cluster-openshift-version", c.OpenshiftVersion, "The version of openshift installed on the cluster. An empty string indicates that the latest stable version should be used") @@ -263,7 +293,7 @@ func (c *DataplaneClusterConfig) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&c.DataPlaneClusterScalingType, "dataplane-cluster-scaling-type", c.DataPlaneClusterScalingType, "Set to use cluster configuration to configure clusters. Its value should be either 'none' for no scaling, 'manual' or 'auto'.") fs.StringVar(&c.ReadOnlyUserListFile, "read-only-user-list-file", c.ReadOnlyUserListFile, "File contains a list of users with read-only permissions to data plane clusters") fs.BoolVar(&c.EnableReadyDataPlaneClustersReconcile, "enable-ready-dataplane-clusters-reconcile", c.EnableReadyDataPlaneClustersReconcile, "Enables reconciliation for data plane clusters in the 'Ready' state") - fs.StringVar(&c.Kubeconfig, "kubeconfig", c.Kubeconfig, "A path to kubeconfig file used for communication with standalone clusters") + c.addKubeconfigFlag(fs) fs.StringVar(&c.CentralOperatorOLMConfig.CatalogSourceNamespace, "central-operator-cs-namespace", c.CentralOperatorOLMConfig.CatalogSourceNamespace, "Central operator catalog source namespace.") fs.StringVar(&c.CentralOperatorOLMConfig.IndexImage, "central-operator-index-image", c.CentralOperatorOLMConfig.IndexImage, "Central operator index image") fs.StringVar(&c.CentralOperatorOLMConfig.Namespace, "central-operator-namespace", c.CentralOperatorOLMConfig.Namespace, "Central operator namespace") diff --git a/internal/dinosaur/pkg/converters/resourceutil.go b/internal/dinosaur/pkg/converters/resourceutil.go new file mode 100644 index 0000000000..c28a9eda15 --- /dev/null +++ b/internal/dinosaur/pkg/converters/resourceutil.go @@ -0,0 +1,116 @@ +package converters + +import ( + "encoding/json" + "fmt" + + "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/private" + "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/public" + "github.com/stackrox/rox/operator/apis/platform/v1alpha1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +// ConvertPrivateScalingToV1 ... +func ConvertPrivateScalingToV1(scaling *private.ManagedCentralAllOfSpecScannerAnalyzerScaling) v1alpha1.ScannerAnalyzerScaling { + if scaling == nil { + return v1alpha1.ScannerAnalyzerScaling{} + } + autoScaling := scaling.AutoScaling + return v1alpha1.ScannerAnalyzerScaling{ + AutoScaling: (*v1alpha1.AutoScalingPolicy)(&autoScaling), // TODO(create-ticket): validate. + Replicas: &scaling.Replicas, + MinReplicas: &scaling.MinReplicas, + MaxReplicas: &scaling.MaxReplicas, + } + +} + +// ConvertPublicScalingToV1 ... +func ConvertPublicScalingToV1(scaling *public.ScannerSpecAnalyzerScaling) (v1alpha1.ScannerAnalyzerScaling, error) { + if scaling == nil { + return v1alpha1.ScannerAnalyzerScaling{}, nil + } + autoScaling := scaling.AutoScaling + return v1alpha1.ScannerAnalyzerScaling{ + AutoScaling: (*v1alpha1.AutoScalingPolicy)(&autoScaling), // TODO(create-ticket): validate. + Replicas: &scaling.Replicas, + MinReplicas: &scaling.MinReplicas, + MaxReplicas: &scaling.MaxReplicas, + }, nil +} + +func qtyAsString(qty resource.Quantity) string { + return (&qty).String() +} + +// ConvertCoreV1ResourceRequirementsToPublic ... +func ConvertCoreV1ResourceRequirementsToPublic(res *v1.ResourceRequirements) public.ResourceRequirements { + return public.ResourceRequirements{ + Limits: public.ResourceList{ + Cpu: qtyAsString(res.Limits[corev1.ResourceCPU]), + Memory: qtyAsString(res.Limits[corev1.ResourceMemory]), + }, + Requests: public.ResourceList{ + Cpu: qtyAsString(res.Requests[corev1.ResourceCPU]), + Memory: qtyAsString(res.Requests[corev1.ResourceMemory]), + }, + } +} + +// ConvertPublicResourceRequirementsToCoreV1 ... +func ConvertPublicResourceRequirementsToCoreV1(res *public.ResourceRequirements) (corev1.ResourceRequirements, error) { + val, err := json.Marshal(res) + if err != nil { + return corev1.ResourceRequirements{}, nil + } + var privateRes private.ResourceRequirements + err = json.Unmarshal(val, &privateRes) + if err != nil { + return corev1.ResourceRequirements{}, nil + } + return ConvertPrivateResourceRequirementsToCoreV1(&privateRes) +} + +// ConvertPrivateResourceRequirementsToCoreV1 ... +func ConvertPrivateResourceRequirementsToCoreV1(res *private.ResourceRequirements) (corev1.ResourceRequirements, error) { + var limitsCPU, limitsMemory, requestsCPU, requestsMemory resource.Quantity + var err error + + if res.Limits.Cpu != "" { + limitsCPU, err = resource.ParseQuantity(res.Limits.Cpu) + if err != nil { + return corev1.ResourceRequirements{}, fmt.Errorf("parsing CPU limit %q: %v", res.Limits.Cpu, err) + } + } + if res.Limits.Memory != "" { + limitsMemory, err = resource.ParseQuantity(res.Limits.Memory) + if err != nil { + return corev1.ResourceRequirements{}, fmt.Errorf("parsing memory limit %q: %v", res.Limits.Memory, err) + } + } + if res.Requests.Cpu != "" { + requestsCPU, err = resource.ParseQuantity(res.Requests.Cpu) + if err != nil { + return corev1.ResourceRequirements{}, fmt.Errorf("parsing CPU request %q: %v", res.Requests.Cpu, err) + } + } + if res.Requests.Memory != "" { + requestsMemory, err = resource.ParseQuantity(res.Requests.Memory) + if err != nil { + return corev1.ResourceRequirements{}, fmt.Errorf("parsing memory requst %q: %v", res.Limits.Memory, err) + } + } + + return corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: limitsCPU, + corev1.ResourceMemory: limitsMemory, + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: requestsCPU, + corev1.ResourceMemory: requestsMemory, + }, + }, nil +} diff --git a/internal/dinosaur/pkg/handlers/admin_dinosaur.go b/internal/dinosaur/pkg/handlers/admin_dinosaur.go index f733d0fbcc..bed4cf19f7 100644 --- a/internal/dinosaur/pkg/handlers/admin_dinosaur.go +++ b/internal/dinosaur/pkg/handlers/admin_dinosaur.go @@ -7,6 +7,8 @@ import ( "github.com/gorilla/mux" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/admin/private" + "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/dbapi" + "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/public" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/config" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/presenters" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/services" @@ -30,6 +32,38 @@ func NewAdminDinosaurHandler(service services.DinosaurService, accountService ac } } +// Create ... +func (h adminDinosaurHandler) Create(w http.ResponseWriter, r *http.Request) { + var dinosaurRequest public.CentralRequestPayload + ctx := r.Context() + convDinosaur := &dbapi.CentralRequest{} + + cfg := &handlers.HandlerConfig{ + MarshalInto: &dinosaurRequest, + Validate: []handlers.Validate{ + handlers.ValidateAsyncEnabled(r, "creating central requests"), + handlers.ValidateLength(&dinosaurRequest.Name, "name", &handlers.MinRequiredFieldLength, &MaxDinosaurNameLength), + ValidDinosaurClusterName(&dinosaurRequest.Name, "name"), + ValidateDinosaurClusterNameIsUnique(r.Context(), &dinosaurRequest.Name, h.service), + ValidateDinosaurClaims(ctx, &dinosaurRequest, convDinosaur), + ValidateCloudProvider(&h.service, convDinosaur, h.providerConfig, "creating central requests"), + handlers.ValidateMultiAZEnabled(&dinosaurRequest.MultiAz, "creating central requests"), + ValidateCentralSpec(ctx, &dinosaurRequest, "central", convDinosaur), + ValidateScannerSpec(ctx, &dinosaurRequest, "scanner", convDinosaur), + }, + Action: func() (interface{}, *errors.ServiceError) { + svcErr := h.service.RegisterDinosaurJob(convDinosaur) + if svcErr != nil { + return nil, svcErr + } + return presenters.PresentDinosaurRequest(convDinosaur), nil + }, + } + + // return 202 status accepted + handlers.Handle(w, r, cfg, http.StatusAccepted) +} + // Get ... func (h adminDinosaurHandler) Get(w http.ResponseWriter, r *http.Request) { cfg := &handlers.HandlerConfig{ diff --git a/internal/dinosaur/pkg/handlers/dinosaur.go b/internal/dinosaur/pkg/handlers/dinosaur.go index cb660792fa..90f9148707 100644 --- a/internal/dinosaur/pkg/handlers/dinosaur.go +++ b/internal/dinosaur/pkg/handlers/dinosaur.go @@ -1,6 +1,7 @@ package handlers import ( + "context" "net/http" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/dbapi" @@ -33,6 +34,36 @@ func NewDinosaurHandler(service services.DinosaurService, providerConfig *config } } +func validateCentralResourcesUnspecified(ctx context.Context, dinosaurRequest *public.CentralRequestPayload) handlers.Validate { + return func() *errors.ServiceError { + if dinosaurRequest.Central.Resources.Limits.Cpu != "" || + dinosaurRequest.Central.Resources.Limits.Memory != "" || + dinosaurRequest.Central.Resources.Requests.Cpu != "" || + dinosaurRequest.Central.Resources.Requests.Memory != "" { + return errors.Forbidden("not allowed to specify central resources") + } + return nil + } +} + +func validateScannerResourcesUnspecified(ctx context.Context, dinosaurRequest *public.CentralRequestPayload) handlers.Validate { + return func() *errors.ServiceError { + if dinosaurRequest.Scanner.Analyzer.Resources.Limits.Cpu != "" || + dinosaurRequest.Scanner.Analyzer.Resources.Limits.Memory != "" || + dinosaurRequest.Scanner.Analyzer.Resources.Requests.Cpu != "" || + dinosaurRequest.Scanner.Analyzer.Resources.Requests.Memory != "" { + return errors.Forbidden("not allowed to specify scanner analyzer resources") + } + if dinosaurRequest.Scanner.Db.Resources.Limits.Cpu != "" || + dinosaurRequest.Scanner.Db.Resources.Limits.Memory != "" || + dinosaurRequest.Scanner.Db.Resources.Requests.Cpu != "" || + dinosaurRequest.Scanner.Db.Resources.Requests.Memory != "" { + return errors.Forbidden("not allowed to specify scanner db resources") + } + return nil + } +} + // Create ... func (h dinosaurHandler) Create(w http.ResponseWriter, r *http.Request) { var dinosaurRequest public.CentralRequestPayload @@ -49,6 +80,8 @@ func (h dinosaurHandler) Create(w http.ResponseWriter, r *http.Request) { ValidateDinosaurClaims(ctx, &dinosaurRequest, convDinosaur), ValidateCloudProvider(&h.service, convDinosaur, h.providerConfig, "creating central requests"), handlers.ValidateMultiAZEnabled(&dinosaurRequest.MultiAz, "creating central requests"), + validateCentralResourcesUnspecified(ctx, &dinosaurRequest), + validateScannerResourcesUnspecified(ctx, &dinosaurRequest), }, Action: func() (interface{}, *errors.ServiceError) { svcErr := h.service.RegisterDinosaurJob(convDinosaur) diff --git a/internal/dinosaur/pkg/handlers/validation.go b/internal/dinosaur/pkg/handlers/validation.go index 30f6343011..24bd7b9e86 100644 --- a/internal/dinosaur/pkg/handlers/validation.go +++ b/internal/dinosaur/pkg/handlers/validation.go @@ -2,6 +2,7 @@ package handlers import ( "context" + "encoding/json" "fmt" "regexp" @@ -11,6 +12,8 @@ import ( "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/services" "github.com/stackrox/acs-fleet-manager/pkg/auth" "github.com/stackrox/acs-fleet-manager/pkg/errors" + "k8s.io/apimachinery/pkg/api/resource" + "github.com/stackrox/acs-fleet-manager/pkg/handlers" coreServices "github.com/stackrox/acs-fleet-manager/pkg/services" ) @@ -112,3 +115,94 @@ func ValidateDinosaurClaims(ctx context.Context, dinosaurRequestPayload *public. return nil } } + +func validateQuantity(qty string, path string) *errors.ServiceError { + if qty == "" { + return nil + } + _, err := resource.ParseQuantity(qty) + if err != nil { + return errors.Validation("invalid resources: failed to parse quantity %q at %s due to: %v", qty, path, err) + } + return nil +} + +// ValidateCentralSpec ... +func ValidateCentralSpec(ctx context.Context, centralRequestPayload *public.CentralRequestPayload, field string, dbCentral *dbapi.CentralRequest) handlers.Validate { + return func() *errors.ServiceError { + // Validate Central resources. + if err := validateQuantity(centralRequestPayload.Central.Resources.Requests.Cpu, "central.resources.requests.cpu"); err != nil { + return err + } + if err := validateQuantity(centralRequestPayload.Central.Resources.Requests.Memory, "central.resources.requests.memory"); err != nil { + return err + } + if err := validateQuantity(centralRequestPayload.Central.Resources.Limits.Cpu, "central.resources.limits.cpu"); err != nil { + return err + } + if err := validateQuantity(centralRequestPayload.Central.Resources.Limits.Cpu, "central.resources.limits.memory"); err != nil { + return err + } + central, err := json.Marshal(centralRequestPayload.Central) + if err != nil { + return errors.Validation("marshaling Central spec failed: %v", err) + } + + if err := json.Unmarshal(central, &dbapi.CentralSpec{}); err != nil { + return errors.Validation("invalid value as Central spec: %v", err) + } + + dbCentral.Central = central + return nil + } +} + +// ValidateScannerSpec ... +func ValidateScannerSpec(ctx context.Context, centralRequestPayload *public.CentralRequestPayload, field string, dbCentral *dbapi.CentralRequest) handlers.Validate { + return func() *errors.ServiceError { + // Validate Scanner Analyzer resources and scaling settings. + if err := validateQuantity(centralRequestPayload.Scanner.Analyzer.Resources.Requests.Cpu, "scanner.analyzer.resources.requests.cpu"); err != nil { + return err + } + if err := validateQuantity(centralRequestPayload.Scanner.Analyzer.Resources.Requests.Memory, "scanner.analyzer.resources.requests.memory"); err != nil { + return err + } + if err := validateQuantity(centralRequestPayload.Scanner.Analyzer.Resources.Limits.Cpu, "scanner.analyzer.resources.limits.cpu"); err != nil { + return err + } + if err := validateQuantity(centralRequestPayload.Scanner.Analyzer.Resources.Limits.Cpu, "scanner.analyzer.resources.limits.memory"); err != nil { + return err + } + if centralRequestPayload.Scanner.Analyzer.Scaling.AutoScaling != "" && + centralRequestPayload.Scanner.Analyzer.Scaling.AutoScaling != "Enabled" && + centralRequestPayload.Scanner.Analyzer.Scaling.AutoScaling != "Disabled" { + return errors.Validation("invalid AutoScaling setting at Scanner.Analyzer.Scaling.AutoScaling, expected 'Enabled' or 'Disabled'") + } + + // Validate Scanner DB resources. + if err := validateQuantity(centralRequestPayload.Scanner.Db.Resources.Requests.Cpu, "scanner.db.resources.requests.cpu"); err != nil { + return err + } + if err := validateQuantity(centralRequestPayload.Scanner.Db.Resources.Requests.Memory, "scanner.db.resources.requests.memory"); err != nil { + return err + } + if err := validateQuantity(centralRequestPayload.Scanner.Db.Resources.Limits.Cpu, "scanner.db.resources.limits.cpu"); err != nil { + return err + } + if err := validateQuantity(centralRequestPayload.Scanner.Db.Resources.Limits.Cpu, "scanner.db.resources.limits.memory"); err != nil { + return err + } + + scanner, err := json.Marshal(centralRequestPayload.Scanner) + if err != nil { + return errors.Validation("marshaling Scanner spec failed: %v", err) + } + + if err := json.Unmarshal(scanner, &dbapi.ScannerSpec{}); err != nil { + return errors.Validation("invalid value as Scanner spec: %v", err) + } + + dbCentral.Scanner = scanner + return nil + } +} diff --git a/internal/dinosaur/pkg/migrations/20220728000000_add_resources_to_centrals.go b/internal/dinosaur/pkg/migrations/20220728000000_add_resources_to_centrals.go new file mode 100644 index 0000000000..9971018a0c --- /dev/null +++ b/internal/dinosaur/pkg/migrations/20220728000000_add_resources_to_centrals.go @@ -0,0 +1,70 @@ +package migrations + +import ( + "fmt" + "time" + + "github.com/go-gormigrate/gormigrate/v2" + "github.com/stackrox/acs-fleet-manager/pkg/api" + "github.com/stackrox/acs-fleet-manager/pkg/db" + "gorm.io/gorm" +) + +func addResourcesToCentralRequest() *gormigrate.Migration { + newColumns := []string{"Central", "Scanner"} + + type CentralRequest struct { + db.Model + Region string `json:"region"` + ClusterID string `json:"cluster_id" gorm:"index"` + CloudProvider string `json:"cloud_provider"` + MultiAZ bool `json:"multi_az"` + Name string `json:"name" gorm:"index"` + Status string `json:"status" gorm:"index"` + SubscriptionID string `json:"subscription_id"` + Owner string `json:"owner" gorm:"index"` + OwnerAccountID string `json:"owner_account_id"` + OwnerUserID string `json:"owner_user_id"` + Host string `json:"host"` + OrganisationID string `json:"organisation_id" gorm:"index"` + FailedReason string `json:"failed_reason"` + PlacementID string `json:"placement_id"` + DesiredCentralVersion string `json:"desired_central_version"` + ActualCentralVersion string `json:"actual_central_version"` + DesiredCentralOperatorVersion string `json:"desired_central_operator_version"` + ActualCentralOperatorVersion string `json:"actual_central_operator_version"` + CentralUpgrading bool `json:"central_upgrading"` + CentralOperatorUpgrading bool `json:"central_operator_upgrading"` + InstanceType string `json:"instance_type"` + QuotaType string `json:"quota_type"` + Routes api.JSON `json:"routes"` + RoutesCreated bool `json:"routes_created"` + Namespace string `json:"namespace"` + RoutesCreationID string `json:"routes_creation_id"` + DeletionTimestamp *time.Time `json:"deletionTimestamp"` + Central api.JSON `json:"central"` + Scanner api.JSON `json:"scanner"` + } + + return &gormigrate.Migration{ + ID: "20220728000000", + Migrate: func(tx *gorm.DB) error { + for _, col := range newColumns { + err := tx.Migrator().AddColumn(&CentralRequest{}, col) + if err != nil { + return fmt.Errorf("adding new column %q: %w", col, err) + } + } + return nil + }, + Rollback: func(tx *gorm.DB) error { + for _, col := range newColumns { + err := tx.Migrator().DropColumn(&CentralRequest{}, col) + if err != nil { + return fmt.Errorf("removing column %q: %w", col, err) + } + } + return nil + }, + } +} diff --git a/internal/dinosaur/pkg/migrations/migrations.go b/internal/dinosaur/pkg/migrations/migrations.go index d121e4250b..cf6cc018aa 100644 --- a/internal/dinosaur/pkg/migrations/migrations.go +++ b/internal/dinosaur/pkg/migrations/migrations.go @@ -30,6 +30,7 @@ var migrations = []*gormigrate.Migration{ addLeaderLease(), sampleMigration(), addOwnerUserIDToCentralRequest(), + addResourcesToCentralRequest(), } // New ... diff --git a/internal/dinosaur/pkg/presenters/dinosaur.go b/internal/dinosaur/pkg/presenters/dinosaur.go index 65a5810ede..be9f7e4615 100644 --- a/internal/dinosaur/pkg/presenters/dinosaur.go +++ b/internal/dinosaur/pkg/presenters/dinosaur.go @@ -1,10 +1,13 @@ package presenters import ( + "encoding/json" "fmt" + "github.com/golang/glog" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/dbapi" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/public" + "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/converters" ) // ConvertDinosaurRequest from payload to DinosaurRequest @@ -22,6 +25,25 @@ func ConvertDinosaurRequest(dinosaurRequestPayload public.CentralRequestPayload, // PresentDinosaurRequest - create CentralRequest in an appropriate format ready to be returned by the API func PresentDinosaurRequest(request *dbapi.CentralRequest) public.CentralRequest { + var central dbapi.CentralSpec + var scanner dbapi.ScannerSpec + + if len(request.Central) > 0 { + if err := json.Unmarshal(request.Central, ¢ral); err != nil { + // Assuming here that what is in the DB is guaranteed to conform to the expected schema. + // TODO: Add error propagation. + glog.Errorf("Failed to unmarshal Central spec: %v", err) + } + } + + if len(request.Scanner) > 0 { + if err := json.Unmarshal(request.Scanner, &scanner); err != nil { + // Assuming here that what is in the DB is guaranteed to conform to the expected schema. + // TODO: Add error propagation. + glog.Errorf("Failed to unmarshal Scanner spec: %v", err) + } + } + return public.CentralRequest{ Id: request.ID, Kind: "CentralRequest", @@ -38,5 +60,16 @@ func PresentDinosaurRequest(request *dbapi.CentralRequest) public.CentralRequest FailedReason: request.FailedReason, Version: request.ActualCentralVersion, InstanceType: request.InstanceType, + Central: public.CentralSpec{ + Resources: converters.ConvertCoreV1ResourceRequirementsToPublic(¢ral.Resources), + }, + Scanner: public.ScannerSpec{ + Analyzer: public.ScannerSpecAnalyzer{ + Resources: converters.ConvertCoreV1ResourceRequirementsToPublic(&scanner.Analyzer.Resources), + }, + Db: public.ScannerSpecDb{ + Resources: converters.ConvertCoreV1ResourceRequirementsToPublic(&scanner.Db.Resources), + }, + }, } } diff --git a/internal/dinosaur/pkg/presenters/managedcentral.go b/internal/dinosaur/pkg/presenters/managedcentral.go index 0b3dc3b4f9..9045b910c0 100644 --- a/internal/dinosaur/pkg/presenters/managedcentral.go +++ b/internal/dinosaur/pkg/presenters/managedcentral.go @@ -1,29 +1,37 @@ package presenters import ( + "encoding/json" "time" + "github.com/golang/glog" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/dbapi" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/api/private" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/config" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" ) -// TODO(create-ticket): implement configurable central and scanner resources -const ( - defaultCentralRequestMemory = "250Mi" - defaultCentralRequestCPU = "250m" - defaultCentralLimitMemory = "4Gi" - defaultCentralLimitCPU = "1000m" - - defaultScannerAnalyzerRequestMemory = "100Mi" - defaultScannerAnalyzerRequestCPU = "250m" - defaultScannerAnalyzerLimitMemory = "2500Mi" - defaultScannerAnalyzerLimitCPU = "2000m" - - defaultScannerAnalyzerAutoScaling = "enabled" - defaultScannerAnalyzerScalingReplicas = 1 - defaultScannerAnalyzerScalingMinReplicas = 1 - defaultScannerAnalyzerScalingMaxReplicas = 3 +var ( + defaultCentralRequestMemory = resource.MustParse("250Mi") + defaultCentralRequestCPU = resource.MustParse("250m") + defaultCentralLimitMemory = resource.MustParse("4Gi") + defaultCentralLimitCPU = resource.MustParse("1000m") + + defaultScannerAnalyzerRequestMemory = resource.MustParse("100Mi") + defaultScannerAnalyzerRequestCPU = resource.MustParse("250m") + defaultScannerAnalyzerLimitMemory = resource.MustParse("2500Mi") + defaultScannerAnalyzerLimitCPU = resource.MustParse("2000m") + + defaultScannerAnalyzerAutoScaling = "Enabled" + defaultScannerAnalyzerScalingReplicas int32 = 1 + defaultScannerAnalyzerScalingMinReplicas int32 = 1 + defaultScannerAnalyzerScalingMaxReplicas int32 = 3 + + defaultScannerDbRequestMemory = resource.MustParse("100Mi") + defaultScannerDbRequestCPU = resource.MustParse("250m") + defaultScannerDbLimitMemory = resource.MustParse("2500Mi") + defaultScannerDbLimitCPU = resource.MustParse("2000m") ) // ManagedCentralPresenter helper service which converts Central DB representation to the private API representation @@ -38,6 +46,30 @@ func NewManagedCentralPresenter(config *config.CentralConfig) *ManagedCentralPre // PresentManagedCentral converts DB representation of Central to the private API representation func (c *ManagedCentralPresenter) PresentManagedCentral(from *dbapi.CentralRequest) private.ManagedCentral { + var central dbapi.CentralSpec + var scanner dbapi.ScannerSpec + + if len(from.Central) > 0 { + err := json.Unmarshal(from.Central, ¢ral) + if err != nil { + // In case of a JSON unmarshaling problem we don't interrupt the complete workflow, instead we drop the resources + // specification as a way of defensive programing. + // TOOD: return error? + glog.Errorf("Failed to unmarshal Central specification for Central request %q/%s: %v", from.Name, from.ClusterID, err) + glog.Errorf("Ignoring Central specification for Central request %q/%s", from.Name, from.ClusterID) + } + } + if len(from.Scanner) > 0 { + err := json.Unmarshal(from.Scanner, &scanner) + if err != nil { + // In case of a JSON unmarshaling problem we don't interrupt the complete workflow, instead we drop the resources + // specification as a way of defensive programing. + // TOOD: return error? + glog.Errorf("Failed to unmarshal Scanner specification for Central request %q/%s: %v", from.Name, from.ClusterID, err) + glog.Errorf("Ignoring Scanner specification for Central request %q/%s", from.Name, from.ClusterID) + } + } + res := private.ManagedCentral{ Id: from.ID, Kind: "ManagedCentral", @@ -79,37 +111,47 @@ func (c *ManagedCentralPresenter) PresentManagedCentral(from *dbapi.CentralReque Central: private.ManagedCentralAllOfSpecCentral{ Resources: private.ResourceRequirements{ Requests: private.ResourceList{ - Cpu: defaultCentralRequestCPU, - Memory: defaultCentralRequestMemory, + Cpu: orDefaultQty(central.Resources.Requests[corev1.ResourceCPU], defaultCentralRequestCPU).String(), + Memory: orDefaultQty(central.Resources.Requests[corev1.ResourceMemory], defaultCentralRequestMemory).String(), }, Limits: private.ResourceList{ - Cpu: defaultCentralLimitCPU, - Memory: defaultCentralLimitMemory, + Cpu: orDefaultQty(central.Resources.Limits[corev1.ResourceCPU], defaultCentralLimitCPU).String(), + Memory: orDefaultQty(central.Resources.Limits[corev1.ResourceMemory], defaultCentralLimitMemory).String(), }, }, }, Scanner: private.ManagedCentralAllOfSpecScanner{ Analyzer: private.ManagedCentralAllOfSpecScannerAnalyzer{ Scaling: private.ManagedCentralAllOfSpecScannerAnalyzerScaling{ - AutoScaling: defaultScannerAnalyzerAutoScaling, - Replicas: defaultScannerAnalyzerScalingReplicas, - MinReplicas: defaultScannerAnalyzerScalingMinReplicas, - MaxReplicas: defaultScannerAnalyzerScalingMaxReplicas, + AutoScaling: orDefaultString(scanner.Analyzer.Scaling.AutoScaling, defaultScannerAnalyzerAutoScaling), + Replicas: orDefaultInt32(scanner.Analyzer.Scaling.Replicas, defaultScannerAnalyzerScalingReplicas), + MinReplicas: orDefaultInt32(scanner.Analyzer.Scaling.MinReplicas, defaultScannerAnalyzerScalingMinReplicas), + MaxReplicas: orDefaultInt32(scanner.Analyzer.Scaling.MaxReplicas, defaultScannerAnalyzerScalingMaxReplicas), }, Resources: private.ResourceRequirements{ Requests: private.ResourceList{ - Cpu: defaultScannerAnalyzerRequestCPU, - Memory: defaultScannerAnalyzerRequestMemory, + Cpu: orDefaultQty(scanner.Analyzer.Resources.Requests[corev1.ResourceCPU], defaultScannerAnalyzerRequestCPU).String(), + Memory: orDefaultQty(scanner.Analyzer.Resources.Requests[corev1.ResourceMemory], defaultScannerAnalyzerRequestMemory).String(), }, Limits: private.ResourceList{ - Cpu: defaultScannerAnalyzerLimitCPU, - Memory: defaultScannerAnalyzerLimitMemory, + Cpu: orDefaultQty(scanner.Analyzer.Resources.Limits[corev1.ResourceCPU], defaultScannerAnalyzerLimitCPU).String(), + Memory: orDefaultQty(scanner.Analyzer.Resources.Limits[corev1.ResourceMemory], defaultScannerAnalyzerLimitMemory).String(), }, }, }, Db: private.ManagedCentralAllOfSpecScannerDb{ // TODO:(create-ticket): add DB configuration values to ManagedCentral Scanner Host: "dbhost.rhacs-psql-instance", + Resources: private.ResourceRequirements{ + Requests: private.ResourceList{ + Cpu: orDefaultQty(scanner.Db.Resources.Requests[corev1.ResourceCPU], defaultScannerDbRequestCPU).String(), + Memory: orDefaultQty(scanner.Db.Resources.Requests[corev1.ResourceMemory], defaultScannerDbRequestMemory).String(), + }, + Limits: private.ResourceList{ + Cpu: orDefaultQty(scanner.Db.Resources.Limits[corev1.ResourceCPU], defaultScannerDbLimitCPU).String(), + Memory: orDefaultQty(scanner.Db.Resources.Limits[corev1.ResourceMemory], defaultScannerDbLimitMemory).String(), + }, + }, }, }, }, @@ -122,3 +164,24 @@ func (c *ManagedCentralPresenter) PresentManagedCentral(from *dbapi.CentralReque return res } + +func orDefaultQty(qty resource.Quantity, def resource.Quantity) *resource.Quantity { + if qty != (resource.Quantity{}) { + return &qty + } + return &def +} + +func orDefaultString(s string, def string) string { + if s != "" { + return s + } + return def +} + +func orDefaultInt32(i int32, def int32) int32 { + if i != 0 { + return i + } + return def +} diff --git a/internal/dinosaur/pkg/routes/route_loader.go b/internal/dinosaur/pkg/routes/route_loader.go index 7434e9a16b..7c933a306f 100644 --- a/internal/dinosaur/pkg/routes/route_loader.go +++ b/internal/dinosaur/pkg/routes/route_loader.go @@ -211,22 +211,29 @@ func (s *options) buildAPIBaseRouter(mainRouter *mux.Router, basePath string, op adminDinosaurHandler := handlers.NewAdminDinosaurHandler(s.Dinosaur, s.AccountService, s.ProviderConfig) adminRouter := apiV1Router.PathPrefix("/admin").Subrouter() + + // TODO(ROX-11683): For now using RH SSO issuer for the admin API, but needs to be re-visited within this ticket. adminRouter.Use(auth.NewRequireIssuerMiddleware().RequireIssuer( []string{s.IAM.GetConfig().InternalSSORealm.ValidIssuerURI}, errors.ErrorNotFound)) adminRouter.Use(auth.NewRolesAuhzMiddleware(s.AdminRoleAuthZConfig).RequireRolesForMethods(errors.ErrorNotFound)) adminRouter.Use(auth.NewAuditLogMiddleware().AuditLog(errors.ErrorNotFound)) - adminRouter.HandleFunc("/dinosaurs", adminDinosaurHandler.List). + adminDinosaursRouter := adminRouter.PathPrefix("/dinosaurs").Subrouter() + + adminDinosaursRouter.HandleFunc("", adminDinosaurHandler.List). Name(logger.NewLogEvent("admin-list-dinosaurs", "[admin] list all dinosaurs").ToString()). Methods(http.MethodGet) - adminRouter.HandleFunc("/dinosaurs/{id}", adminDinosaurHandler.Get). + adminDinosaursRouter.HandleFunc("/{id}", adminDinosaurHandler.Get). Name(logger.NewLogEvent("admin-get-dinosaur", "[admin] get dinosaur by id").ToString()). Methods(http.MethodGet) - adminRouter.HandleFunc("/dinosaurs/{id}", adminDinosaurHandler.Delete). + adminDinosaursRouter.HandleFunc("/{id}", adminDinosaurHandler.Delete). Name(logger.NewLogEvent("admin-delete-dinosaur", "[admin] delete dinosaur by id").ToString()). Methods(http.MethodDelete) - adminRouter.HandleFunc("/dinosaurs/{id}", adminDinosaurHandler.Update). + adminDinosaursRouter.HandleFunc("/{id}", adminDinosaurHandler.Update). Name(logger.NewLogEvent("admin-update-dinosaur", "[admin] update dinosaur by id").ToString()). Methods(http.MethodPatch) + adminCreateRouter := adminDinosaursRouter.NewRoute().Subrouter() + adminCreateRouter.HandleFunc("", adminDinosaurHandler.Create).Methods(http.MethodPost) + return nil } diff --git a/openapi/fleet-manager-private.yaml b/openapi/fleet-manager-private.yaml index db2f628ecc..aa461cd6be 100644 --- a/openapi/fleet-manager-private.yaml +++ b/openapi/fleet-manager-private.yaml @@ -22,7 +22,7 @@ tags: paths: # Endpoints for data plane communications - '/api/rhacs/v1/agent-clusters/{id}/status': + "/api/rhacs/v1/agent-clusters/{id}/status": put: tags: - Agent Clusters @@ -33,36 +33,36 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DataPlaneClusterUpdateStatusRequest' + $ref: "#/components/schemas/DataPlaneClusterUpdateStatusRequest" required: true responses: - '200': + "200": description: Cluster status is updated - '400': + "400": content: application/json: schema: - $ref: 'fleet-manager.yaml#/components/schemas/Error' + $ref: "fleet-manager.yaml#/components/schemas/Error" examples: 400InvalidIdExample: - $ref: '#/components/examples/400InvalidIdExample' + $ref: "#/components/examples/400InvalidIdExample" description: id value is not valid - '404': + "404": content: application/json: schema: - $ref: 'fleet-manager.yaml#/components/schemas/Error' + $ref: "fleet-manager.yaml#/components/schemas/Error" examples: 404Example: - $ref: 'fleet-manager.yaml#/components/examples/404Example' + $ref: "fleet-manager.yaml#/components/examples/404Example" # This is deliberate to hide the endpoints for unauthorised users description: Auth token is not valid. security: - - Bearer: [ ] + - Bearer: [] operationId: updateAgentClusterStatus summary: Update the status of an agent cluster - '/api/rhacs/v1/agent-clusters/{id}/centrals/status': + "/api/rhacs/v1/agent-clusters/{id}/centrals/status": put: tags: - Agent Clusters @@ -73,112 +73,111 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DataPlaneCentralStatusUpdateRequest' + $ref: "#/components/schemas/DataPlaneCentralStatusUpdateRequest" required: true responses: - '200': + "200": description: Status is updated for Centrals - '400': + "400": content: application/json: schema: - $ref: 'fleet-manager.yaml#/components/schemas/Error' + $ref: "fleet-manager.yaml#/components/schemas/Error" examples: 400InvalidIdExample: - $ref: '#/components/examples/400InvalidIdExample' + $ref: "#/components/examples/400InvalidIdExample" description: id value is not valid - '404': + "404": content: application/json: schema: - $ref: 'fleet-manager.yaml#/components/schemas/Error' + $ref: "fleet-manager.yaml#/components/schemas/Error" examples: 404Example: - $ref: 'fleet-manager.yaml#/components/examples/404Example' + $ref: "fleet-manager.yaml#/components/examples/404Example" # This is deliberate to hide the endpoints for unauthorised users description: Auth token is not valid. security: - - Bearer: [ ] + - Bearer: [] operationId: updateCentralClusterStatus summary: Update the status of Centrals on an agent cluster - '/api/rhacs/v1/agent-clusters/{id}/centrals': + "/api/rhacs/v1/agent-clusters/{id}/centrals": get: tags: - Agent Clusters parameters: - $ref: "fleet-manager.yaml#/components/parameters/id" responses: - '200': + "200": description: The list of the ManagedCentrals for the specified agent cluster content: application/json: schema: - $ref: '#/components/schemas/ManagedCentralList' - '400': + $ref: "#/components/schemas/ManagedCentralList" + "400": content: application/json: schema: - $ref: 'fleet-manager.yaml#/components/schemas/Error' + $ref: "fleet-manager.yaml#/components/schemas/Error" examples: 400InvalidIdExample: - $ref: '#/components/examples/400InvalidIdExample' + $ref: "#/components/examples/400InvalidIdExample" description: id value is not valid - '404': + "404": content: application/json: schema: - $ref: 'fleet-manager.yaml#/components/schemas/Error' + $ref: "fleet-manager.yaml#/components/schemas/Error" examples: 404Example: - $ref: 'fleet-manager.yaml#/components/examples/404Example' + $ref: "fleet-manager.yaml#/components/examples/404Example" # This is deliberate to hide the endpoints for unauthorised users description: Auth token is not valid. security: - - Bearer: [ ] + - Bearer: [] operationId: getCentrals summary: Get the list of ManagedaCentrals for the specified agent cluster - '/api/rhacs/v1/agent-clusters/{id}': + "/api/rhacs/v1/agent-clusters/{id}": get: tags: - Agent Clusters parameters: - $ref: "fleet-manager.yaml#/components/parameters/id" responses: - '200': + "200": description: The Data Plane Cluster Agent configuration content: application/json: schema: - $ref: '#/components/schemas/DataplaneClusterAgentConfig' - '400': + $ref: "#/components/schemas/DataplaneClusterAgentConfig" + "400": content: application/json: schema: - $ref: 'fleet-manager.yaml#/components/schemas/Error' + $ref: "fleet-manager.yaml#/components/schemas/Error" examples: 400InvalidIdExample: - $ref: '#/components/examples/400InvalidIdExample' + $ref: "#/components/examples/400InvalidIdExample" description: id value is not valid - '404': + "404": content: application/json: schema: - $ref: 'fleet-manager.yaml#/components/schemas/Error' + $ref: "fleet-manager.yaml#/components/schemas/Error" examples: 404Example: - $ref: 'fleet-manager.yaml#/components/examples/404Example' + $ref: "fleet-manager.yaml#/components/examples/404Example" # This is deliberate to hide the endpoints for unauthorised users description: Auth token is not valid. security: - - Bearer: [ ] + - Bearer: [] operationId: getDataPlaneClusterAgentConfig summary: Get the data plane cluster agent configuration components: schemas: - ListReference: required: - kind @@ -320,8 +319,11 @@ components: properties: host: type: string + resources: + $ref: "#/components/schemas/ResourceRequirements" requestStatus: type: string + ManagedCentralList: description: >- A list of ManagedCentral @@ -331,7 +333,7 @@ components: example: kind: "ManagedCentralList" items: - $ref: '#/components/examples/ManagedCentralExample' + $ref: "#/components/examples/ManagedCentralExample" properties: items: type: array @@ -373,8 +375,8 @@ components: items: type: string required: - - ready - - version + - ready + - version DataPlaneCentralStatus: description: "Schema of the status object for a Central" type: object @@ -412,13 +414,13 @@ components: router: type: string example: - $ref: '#/components/examples/DataPlaneCentralStatusRequestExample' + $ref: "#/components/examples/DataPlaneCentralStatusRequestExample" DataPlaneCentralStatusUpdateRequest: description: "Schema for the request to update the statuses of Central clusters from data plane" type: object additionalProperties: - $ref: '#/components/schemas/DataPlaneCentralStatus' + $ref: "#/components/schemas/DataPlaneCentralStatus" DataplaneClusterAgentConfig: description: "Configuration for the data plane cluster agent" diff --git a/openapi/fleet-manager.yaml b/openapi/fleet-manager.yaml index ada23bece0..73128038a9 100644 --- a/openapi/fleet-manager.yaml +++ b/openapi/fleet-manager.yaml @@ -22,7 +22,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/VersionMetadata' + $ref: "#/components/schemas/VersionMetadata" description: Version metadata summary: Returns the version metadata @@ -34,7 +34,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" description: Get error by Id summary: Returns the error by id tags: @@ -50,7 +50,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ErrorList' + $ref: "#/components/schemas/ErrorList" description: List of possible errors summary: Returns the list of possible API errors tags: @@ -64,20 +64,20 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ServiceStatus' + $ref: "#/components/schemas/ServiceStatus" description: Ok "500": description: Unexpected error occurred content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 500Example: - $ref: '#/components/examples/500Example' + $ref: "#/components/examples/500Example" summary: Returns the status of resources, such as whether maximum service capacity has been reached security: - - Bearer: [ ] + - Bearer: [] /api/rhacs/v1/centrals/{id}: get: operationId: getCentralById @@ -87,51 +87,51 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/CentralRequest' + $ref: "#/components/schemas/CentralRequest" examples: CentralRequestGetResponseExample: - $ref: '#/components/examples/CentralRequestExample' + $ref: "#/components/examples/CentralRequestExample" CentralRequestGetResponseWithFailedCreationStatusExample: - $ref: '#/components/examples/CentralRequestFailedCreationStatusExample' + $ref: "#/components/examples/CentralRequestFailedCreationStatusExample" description: Central request found by ID "401": content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 401Example: - $ref: '#/components/examples/401Example' + $ref: "#/components/examples/401Example" description: Auth token is invalid "403": content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 403Example: - $ref: '#/components/examples/403Example' + $ref: "#/components/examples/403Example" description: User not authorized to access the service "404": content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 404Example: - $ref: '#/components/examples/404Example' + $ref: "#/components/examples/404Example" description: No Central request with specified ID exists "500": content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 500Example: - $ref: '#/components/examples/500Example' + $ref: "#/components/examples/500Example" description: Unexpected error occurred security: - - Bearer: [ ] + - Bearer: [] summary: Returns a Central request by ID delete: operationId: deleteCentralById @@ -153,50 +153,50 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 400DeletionExample: - $ref: '#/components/examples/400DeletionExample' + $ref: "#/components/examples/400DeletionExample" description: Validation errors occurred "401": content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 401Example: - $ref: '#/components/examples/401Example' + $ref: "#/components/examples/401Example" description: Auth token is invalid "403": content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 403Example: - $ref: '#/components/examples/403Example' + $ref: "#/components/examples/403Example" description: User not authorized to access the service "404": content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 404DeleteExample: - $ref: '#/components/examples/404DeleteExample' + $ref: "#/components/examples/404DeleteExample" description: No Central request with specified ID exists "500": content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 500DeleteExample: - $ref: '#/components/examples/500DeleteExample' + $ref: "#/components/examples/500DeleteExample" description: Unexpected error occurred summary: Deletes a Central request by ID security: - - Bearer: [ ] + - Bearer: [] parameters: - $ref: "#/components/parameters/id" /api/rhacs/v1/centrals: @@ -215,211 +215,211 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/CentralRequestPayload' + $ref: "#/components/schemas/CentralRequestPayload" examples: USRegion: - $ref: '#/components/examples/USRegionExample' + $ref: "#/components/examples/USRegionExample" EURegion: - $ref: '#/components/examples/EURegionExample' + $ref: "#/components/examples/EURegionExample" required: true responses: "202": content: application/json: schema: - $ref: '#/components/schemas/CentralRequest' + $ref: "#/components/schemas/CentralRequest" examples: CentralRequestPostResponseExample: - $ref: '#/components/examples/CentralRequestExample' + $ref: "#/components/examples/CentralRequestExample" description: Accepted "400": content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 400CreationExample: - $ref: '#/components/examples/400CreationExample' + $ref: "#/components/examples/400CreationExample" description: Validation errors occurred "401": content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 401Example: - $ref: '#/components/examples/401Example' + $ref: "#/components/examples/401Example" description: Auth token is invalid "403": content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 403Example: - $ref: '#/components/examples/403Example' + $ref: "#/components/examples/403Example" 403MaxAllowedInstanceReachedExample: - $ref: '#/components/examples/403MaxAllowedInstanceReachedExample' + $ref: "#/components/examples/403MaxAllowedInstanceReachedExample" 403TermsNotAcceptedExample: - $ref: '#/components/examples/403TermsNotAcceptedExample' + $ref: "#/components/examples/403TermsNotAcceptedExample" description: User forbidden either because the user is not authorized to access the service or because the maximum number of instances that can be created by this user has been reached. "404": content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 404Example: - $ref: '#/components/examples/404Example' + $ref: "#/components/examples/404Example" description: The requested resource doesn't exist "409": content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 409NameConflictExample: - $ref: '#/components/examples/409NameConflictExample' + $ref: "#/components/examples/409NameConflictExample" description: A conflict has been detected in the creation of this resource "500": content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 500Example: - $ref: '#/components/examples/500Example' + $ref: "#/components/examples/500Example" description: An unexpected error occurred while creating the Central request security: - - Bearer: [ ] + - Bearer: [] summary: Creates a Central request get: summary: Returns a list of Central requests description: Only returns those centrals that are owned by the organisation of the user authenticated for the request. operationId: getCentrals security: - - Bearer: [ ] + - Bearer: [] responses: "200": description: A list of Central requests content: application/json: schema: - $ref: '#/components/schemas/CentralRequestList' + $ref: "#/components/schemas/CentralRequestList" examples: CentralRequestListExample: - $ref: '#/components/examples/CentralRequestListExample' + $ref: "#/components/examples/CentralRequestListExample" "400": description: Bad request content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: InvalidQueryExample: - $ref: '#/components/examples/400InvalidQueryExample' + $ref: "#/components/examples/400InvalidQueryExample" "401": description: Auth token is invalid content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 401Example: - $ref: '#/components/examples/401Example' + $ref: "#/components/examples/401Example" "403": content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 403Example: - $ref: '#/components/examples/403Example' + $ref: "#/components/examples/403Example" description: User not authorized to access the service "500": description: Unexpected error occurred content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 500Example: - $ref: '#/components/examples/500Example' + $ref: "#/components/examples/500Example" parameters: - - $ref: '#/components/parameters/page' - - $ref: '#/components/parameters/size' - - $ref: '#/components/parameters/orderBy' - - $ref: '#/components/parameters/search' + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/size" + - $ref: "#/components/parameters/orderBy" + - $ref: "#/components/parameters/search" /api/rhacs/v1/cloud_providers: get: summary: Returns the list of supported cloud providers operationId: getCloudProviders security: - - Bearer: [ ] + - Bearer: [] responses: - '200': + "200": description: Returned list of supported cloud providers content: application/json: schema: - $ref: '#/components/schemas/CloudProviderList' - '401': + $ref: "#/components/schemas/CloudProviderList" + "401": description: Auth token is invalid content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 401Example: - $ref: '#/components/examples/401Example' - '500': + $ref: "#/components/examples/401Example" + "500": description: Unexpected error occurred content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 500Example: - $ref: '#/components/examples/500Example' + $ref: "#/components/examples/500Example" parameters: - - $ref: '#/components/parameters/page' - - $ref: '#/components/parameters/size' + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/size" /api/rhacs/v1/cloud_providers/{id}/regions: get: summary: Returns the list of supported regions of the supported cloud provider operationId: getCloudProviderRegions security: - - Bearer: [ ] + - Bearer: [] responses: - '200': + "200": description: Returned list of supported cloud provider regions content: application/json: schema: - $ref: '#/components/schemas/CloudRegionList' - '401': + $ref: "#/components/schemas/CloudRegionList" + "401": description: Auth token is invalid content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 401Example: - $ref: '#/components/examples/401Example' - '500': + $ref: "#/components/examples/401Example" + "500": description: Unexpected error occurred content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 500Example: - $ref: '#/components/examples/500Example' + $ref: "#/components/examples/500Example" parameters: - $ref: "#/components/parameters/id" - - $ref: '#/components/parameters/page' - - $ref: '#/components/parameters/size' - - $ref: '#/components/parameters/instance_type' + - $ref: "#/components/parameters/page" + - $ref: "#/components/parameters/size" + - $ref: "#/components/parameters/instance_type" # # These are the user-facing related endpoints # @@ -428,32 +428,32 @@ paths: summary: Returns metrics with timeseries range query by Central ID operationId: getMetricsByRangeQuery security: - - Bearer: [ ] + - Bearer: [] responses: - '200': + "200": description: Returned JSON array of Prometheus metrics objects from observatorium content: application/json: schema: - $ref: '#/components/schemas/MetricsRangeQueryList' - '401': + $ref: "#/components/schemas/MetricsRangeQueryList" + "401": description: Auth token is invalid content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 401Example: - $ref: '#/components/examples/401Example' - '500': + $ref: "#/components/examples/401Example" + "500": description: Unexpected error occurred content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 500Example: - $ref: '#/components/examples/500Example' + $ref: "#/components/examples/500Example" parameters: - $ref: "#/components/parameters/id" - $ref: "#/components/parameters/duration" @@ -464,32 +464,32 @@ paths: summary: Returns metrics with instant query by Central ID operationId: getMetricsByInstantQuery security: - - Bearer: [ ] + - Bearer: [] responses: - '200': + "200": description: Returned JSON array of Prometheus metrics objects from observatorium content: application/json: schema: - $ref: '#/components/schemas/MetricsInstantQueryList' - '401': + $ref: "#/components/schemas/MetricsInstantQueryList" + "401": description: Auth token is invalid content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 401Example: - $ref: '#/components/examples/401Example' - '500': + $ref: "#/components/examples/401Example" + "500": description: Unexpected error occurred content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 500Example: - $ref: '#/components/examples/500Example' + $ref: "#/components/examples/500Example" parameters: - $ref: "#/components/parameters/id" - $ref: "#/components/parameters/filters" @@ -498,50 +498,50 @@ paths: summary: Returns all metrics in scrapeable format for a given Central ID operationId: federateMetrics security: - - Bearer: [ ] + - Bearer: [] responses: - '200': + "200": description: Returned Central metrics in a Prometheus text format content: text/plain: schema: type: string - '400': + "400": description: Bad request content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: MissingParameterExample: - $ref: '#/components/examples/400MissingParameterExample' - '401': + $ref: "#/components/examples/400MissingParameterExample" + "401": description: Auth token is invalid content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 401Example: - $ref: '#/components/examples/401Example' - '404': + $ref: "#/components/examples/401Example" + "404": description: Central ID not found content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 404Example: - $ref: '#/components/examples/404Example' - '500': + $ref: "#/components/examples/404Example" + "500": description: Unexpected error occurred content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" examples: 500Example: - $ref: '#/components/examples/500Example' + $ref: "#/components/examples/500Example" parameters: - $ref: "#/components/parameters/id" @@ -593,6 +593,20 @@ components: type: array items: $ref: "#/components/schemas/Error" + ResourceList: + type: object + properties: + cpu: + type: string + memory: + type: string + ResourceRequirements: + type: object + properties: + requests: + $ref: "#/components/schemas/ResourceList" + limits: + $ref: "#/components/schemas/ResourceList" CentralRequest: allOf: - $ref: "#/components/schemas/ObjectReference" @@ -629,6 +643,10 @@ components: type: string instance_type: type: string + central: + $ref: "#/components/schemas/CentralSpec" + scanner: + $ref: "#/components/schemas/ScannerSpec" example: $ref: "#/components/examples/CentralRequestExample" CentralRequestList: @@ -641,13 +659,48 @@ components: size: "1" total: "1" item: - $ref: '#/components/examples/CentralRequestExample' + $ref: "#/components/examples/CentralRequestExample" properties: items: type: array items: allOf: - $ref: "#/components/schemas/CentralRequest" + CentralSpec: + type: object + properties: + resources: + $ref: "#/components/schemas/ResourceRequirements" + ScannerSpec: + type: object + properties: + analyzer: + type: object + properties: + resources: + $ref: "#/components/schemas/ResourceRequirements" + scaling: + type: object + properties: + autoScaling: + type: string + replicas: + type: integer + format: int32 + minimum: 1 + minReplicas: + type: integer + format: int32 + minimum: 1 + maxReplicas: + type: integer + format: int32 + minimum: 1 + db: + type: object + properties: + resources: + $ref: "#/components/schemas/ResourceRequirements" VersionMetadata: allOf: - $ref: "#/components/schemas/ObjectReference" @@ -695,11 +748,15 @@ components: description: Set this to true to configure the Central component to be multiAZ type: boolean name: - description: 'The name of the Central component. It must consist of lower-case alphanumeric characters or ''-'', start with an alphabetic character, and end with an alphanumeric character, and can not be longer than 32 characters.' + description: "The name of the Central component. It must consist of lower-case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character, and can not be longer than 32 characters." type: string region: description: The region where the Central component cluster will be created in type: string + central: + $ref: "#/components/schemas/CentralSpec" + scanner: + $ref: "#/components/schemas/ScannerSpec" CloudProviderList: allOf: - $ref: "#/components/schemas/List" @@ -710,7 +767,7 @@ components: size: "1" total: "1" item: - $ref: '#/components/examples/CloudProviderExample' + $ref: "#/components/examples/CloudProviderExample" properties: items: type: array @@ -727,7 +784,7 @@ components: size: "1" total: "1" item: - $ref: '#/components/examples/CloudRegionExample' + $ref: "#/components/examples/CloudRegionExample" properties: items: type: array @@ -735,43 +792,43 @@ components: allOf: - $ref: "#/components/schemas/CloudRegion" CloudProvider: - description: 'Cloud provider.' + description: "Cloud provider." properties: kind: - description: 'Indicates the type of this object. Will be ''CloudProvider'' link.' + description: "Indicates the type of this object. Will be 'CloudProvider' link." type: string id: - description: 'Unique identifier of the object.' + description: "Unique identifier of the object." type: string display_name: - description: 'Name of the cloud provider for display purposes.' + description: "Name of the cloud provider for display purposes." type: string name: - description: 'Human friendly identifier of the cloud provider, for example `aws`.' + description: "Human friendly identifier of the cloud provider, for example `aws`." type: string enabled: - description: 'Whether the cloud provider is enabled for deploying an OSD cluster.' + description: "Whether the cloud provider is enabled for deploying an OSD cluster." type: boolean required: - enabled CloudRegion: - description: 'Description of a region of a cloud provider.' + description: "Description of a region of a cloud provider." properties: kind: - description: 'Indicates the type of this object. Will be ''CloudRegion''.' + description: "Indicates the type of this object. Will be 'CloudRegion'." type: string id: - description: 'Unique identifier of the object.' + description: "Unique identifier of the object." type: string display_name: - description: 'Name of the region for display purposes, for example `N. Virginia`.' + description: "Name of the region for display purposes, for example `N. Virginia`." type: string enabled: - description: 'Whether the region is enabled for deploying an OSD cluster.' + description: "Whether the region is enabled for deploying an OSD cluster." type: boolean default: false supported_instance_types: - description: 'The Central component instance types supported by this region.' + description: "The Central component instance types supported by this region." type: array items: type: string @@ -786,7 +843,7 @@ components: kind: "MetricsRangeQueryList" id: "1nbpS70HduPe4l0to8jSg2CLzfu" items: - $ref: '#/components/examples/MetricsRangeQueryExample' + $ref: "#/components/examples/MetricsRangeQueryExample" properties: kind: type: string @@ -807,7 +864,7 @@ components: values: type: array items: - $ref: '#/components/schemas/values' + $ref: "#/components/schemas/values" values: type: object properties: @@ -826,7 +883,7 @@ components: kind: "MetricsInstantQueryList" id: "1nbpS70HduPe4l0to8jSg2CLzfu" items: - $ref: '#/components/examples/MetricsInstantQueryExample' + $ref: "#/components/examples/MetricsInstantQueryExample" properties: kind: type: string @@ -899,7 +956,7 @@ components: type: array items: type: string - default: [ ] + default: [] page: name: page in: query @@ -1219,7 +1276,8 @@ components: kind: "Error" href: "/api/rhacs/v1/errors/9" code: "RHACS-MGMT-9" - reason: "error deleting syncset: OCM-EX-9: failed to delete syncset: ext-serviceapi-1ix03lndlmq0qfc7sita5sljv8e + reason: + "error deleting syncset: OCM-EX-9: failed to delete syncset: ext-serviceapi-1ix03lndlmq0qfc7sita5sljv8e for cluster id: 1g5d88q0lrcdv4g7alb7slfgnj3dhbsj%!(EXTRA *errors.Error=identifier is '404', code is 'CLUSTERS-MGMT-404' and operation identifier is '1g5or50viu07oealuehrkc26dgftj1ac': Cluster '1g5d88q0lrcdv4g7alb7slfgnj3dhbsj' not found)" diff --git a/pkg/auth/roles_authz.go b/pkg/auth/roles_authz.go index fa8bbe54d5..07b07bf63a 100644 --- a/pkg/auth/roles_authz.go +++ b/pkg/auth/roles_authz.go @@ -81,7 +81,7 @@ func readRoleAuthZConfigFile(file string, val *RoleConfig) error { return nil } -var allowedHTTPMethods = []string{http.MethodGet, http.MethodPatch, http.MethodDelete} +var allowedHTTPMethods = []string{http.MethodGet, http.MethodPatch, http.MethodDelete, http.MethodPost} func validateRolesConfiguration(configs []RolesConfiguration) error { for _, config := range configs {