Skip to content

Commit

Permalink
feat: add alloydbomni service account validation
Browse files Browse the repository at this point in the history
  • Loading branch information
rriski committed Dec 12, 2024
1 parent 8762ce7 commit 430d433
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 12 deletions.
5 changes: 5 additions & 0 deletions api/v1alpha1/alloydbomni_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import (
type AlloyDBOmniSpec struct {
ServiceCommonSpec `json:",inline"`

// +kubebuilder:validation:Schemaless
// +kubebuilder:validation:Type=string
// Your [Google service account key](https://cloud.google.com/iam/docs/service-account-creds#key-types) in JSON format.
ServiceAccountCredentials string `json:"serviceAccountCredentials,omitempty"`

// AlloyDBOmni specific user configuration options
UserConfig *alloydbomni.AlloydbomniUserConfig `json:"userConfig,omitempty"`
}
Expand Down
16 changes: 14 additions & 2 deletions api/v1alpha1/alloydbomni_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ package v1alpha1

import (
"errors"
"fmt"

"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"

alloydbomniUtils "github.com/aiven/aiven-operator/utils/alloydbomni"
)

// log is for logging in this package.
Expand Down Expand Up @@ -37,13 +40,22 @@ var _ webhook.Validator = &AlloyDBOmni{}
func (in *AlloyDBOmni) ValidateCreate() error {
alloydbomnilog.Info("validate create", "name", in.Name)

return in.Spec.Validate()
if err := alloydbomniUtils.ValidateServiceAccountCredentials(in.Spec.ServiceAccountCredentials); err != nil {
return fmt.Errorf("invalid serviceAccountCredentials: %w", err)
}

return nil
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (in *AlloyDBOmni) ValidateUpdate(old runtime.Object) error {
alloydbomnilog.Info("validate update", "name", in.Name)
return in.Spec.Validate()

if err := alloydbomniUtils.ValidateServiceAccountCredentials(in.Spec.ServiceAccountCredentials); err != nil {
return fmt.Errorf("invalid serviceAccountCredentials: %w", err)
}

return nil
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ spec:
description: Identifier of the VPC the service should be in, if any.
maxLength: 36
type: string
serviceAccountCredentials:
description:
Your [Google service account key](https://cloud.google.com/iam/docs/service-account-creds#key-types)
in JSON format.
type: string
serviceIntegrations:
description:
Service integrations to specify when creating a service.
Expand Down
5 changes: 5 additions & 0 deletions config/crd/bases/aiven.io_alloydbomnis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ spec:
description: Identifier of the VPC the service should be in, if any.
maxLength: 36
type: string
serviceAccountCredentials:
description:
Your [Google service account key](https://cloud.google.com/iam/docs/service-account-creds#key-types)
in JSON format.
type: string
serviceIntegrations:
description:
Service integrations to specify when creating a service.
Expand Down
1 change: 1 addition & 0 deletions docs/docs/api-reference/alloydbomni.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ The removal of this field does not change the value.
- [`maintenanceWindowTime`](#spec.maintenanceWindowTime-property){: name='spec.maintenanceWindowTime-property'} (string, MaxLength: 8). Time of day when maintenance operations should be performed. UTC time in HH:mm:ss format.
- [`projectVPCRef`](#spec.projectVPCRef-property){: name='spec.projectVPCRef-property'} (object). ProjectVPCRef reference to ProjectVPC resource to use its ID as ProjectVPCID automatically. See below for [nested schema](#spec.projectVPCRef).
- [`projectVpcId`](#spec.projectVpcId-property){: name='spec.projectVpcId-property'} (string, MaxLength: 36). Identifier of the VPC the service should be in, if any.
- [`serviceAccountCredentials`](#spec.serviceAccountCredentials-property){: name='spec.serviceAccountCredentials-property'} (string). Your [Google service account key](https://cloud.google.com/iam/docs/service-account-creds#key-types) in JSON format.
- [`serviceIntegrations`](#spec.serviceIntegrations-property){: name='spec.serviceIntegrations-property'} (array of objects, Immutable, MaxItems: 1). Service integrations to specify when creating a service. Not applied after initial service creation. See below for [nested schema](#spec.serviceIntegrations).
- [`tags`](#spec.tags-property){: name='spec.tags-property'} (object, AdditionalProperties: string). Tags are key-value pairs that allow you to categorize services.
- [`technicalEmails`](#spec.technicalEmails-property){: name='spec.technicalEmails-property'} (array of objects, MaxItems: 10). Defines the email addresses that will receive alerts about upcoming maintenance updates or warnings about service instability. See below for [nested schema](#spec.technicalEmails).
Expand Down
22 changes: 12 additions & 10 deletions generators/docs/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,17 @@ func newSchemaValidator(kind string, crd []byte) (schemaValidator, error) {
// If not to do so, new properties allowed on validation,
// but won't work when applied with kubectl
func patchSchema(m map[string]any) map[string]any {
if m["type"].(string) != "object" {
t, ok := m["type"].(string)
if !ok || t != "object" {
return m
}

if p, ok := m["properties"]; ok {
prop := p.(map[string]any)
for k, v := range prop {
vv := v.(map[string]any)
if p, ok := m["properties"].(map[string]any); ok {
for k, v := range p {
vv, ok := v.(map[string]any)
if !ok {
continue
}

// metadata schema is empty, replaces with a good one
if k == "metadata" && len(vv) == 1 {
Expand All @@ -133,14 +136,13 @@ func patchSchema(m map[string]any) map[string]any {
continue
}

prop[k] = patchSchema(vv)
p[k] = patchSchema(vv)
}
m["properties"] = prop
m["properties"] = p
}

if i, ok := m["items"]; ok {
items := i.(map[string]any)
m["items"] = patchSchema(items)
if i, ok := m["items"].(map[string]any); ok {
m["items"] = patchSchema(i)
}

if _, ok := m["additionalProperties"]; !ok {
Expand Down
103 changes: 103 additions & 0 deletions utils/alloydbomni/service_account_credentials_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package alloydbomniUtils

import (
"errors"

"github.com/xeipuuv/gojsonschema"
)

func ValidateServiceAccountCredentials(i interface{}) error {
s, ok := i.(string)
if !ok {
return errors.New("expected input to be a string")
}

r, err := gojsonschema.Validate(
gojsonschema.NewStringLoader(serviceAccountCredentialsSchema),
gojsonschema.NewStringLoader(s),
)
if err != nil {
return err
}

if !r.Valid() {
var errMsg string
for _, e := range r.Errors() {
errMsg += e.String() + "\n"
}
return errors.New(errMsg)
}

return nil
}

// trunk-ignore-all(gitleaks/private-key)
const serviceAccountCredentialsSchema = `{
"title": "Google service account credentials map",
"type": "object",
"properties": {
"type": {
"type": "string",
"title": "Credentials type",
"description": "Always service_account for credentials created in Gcloud console or CLI",
"example": "service_account"
},
"project_id": {
"type": "string",
"title": "Gcloud project id",
"example": "some-my-project"
},
"private_key_id": {
"type": "string",
"title": "Hexadecimal ID number of your private key",
"example": "5fdeb02a11ddf081930ac3ac60bf376a0aef8fad"
},
"private_key": {
"type": "string",
"title": "PEM-encoded private key",
"example": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
},
"client_email": {
"type": "string",
"title": "Email of the service account",
"example": "my-service-account@some-my-project.iam.gserviceaccount.com"
},
"client_id": {
"type": "string",
"title": "Numeric client id for this service account",
"example": "103654484443722885992"
},
"auth_uri": {
"type": "string",
"title": "The authentication endpoint of Google",
"example": "https://accounts.google.com/o/oauth2/auth"
},
"token_uri": {
"type": "string",
"title": "The token lease endpoint of Google",
"example": "https://accounts.google.com/o/oauth2/token"
},
"auth_provider_x509_cert_url": {
"type": "string",
"title": "The certificate service of Google",
"example": "https://www.googleapis.com/oauth2/v1/certs"
},
"client_x509_cert_url": {
"type": "string",
"title": "Certificate URL for your service account",
"example": "https://www.googleapis.com/robot/v1/metadata/x509/my-service-account%40some-my-project.iam.gserviceaccount.com"
},
"universe_domain": {
"type": "string",
"title": "The universe domain",
"description": "The universe domain. The default universe domain is googleapis.com."
}
},
"required": [
"private_key_id",
"private_key",
"client_email",
"client_id"
],
"additionalProperties": false
}`
61 changes: 61 additions & 0 deletions utils/alloydbomni/service_account_credentials_validator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package alloydbomniUtils

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestValidateServiceAccountCredentials(t *testing.T) {
cases := []struct {
name string
input string
expected string
}{
{
name: "valid",
input: `{
"private_key_id": "0",
"private_key": "1",
"client_email": "2",
"client_id": "3"
}`,
expected: "",
},
{
name: "invalid, empty",
input: `{}`,
expected: "(root): private_key_id is required\n(root): private_key is required\n(root): client_email is required\n(root): client_id is required\n",
},
{
name: "missing private_key_id",
input: `{
"private_key": "1",
"client_email": "2",
"client_id": "3"
}`,
expected: "(root): private_key_id is required\n",
},
{
name: "invalid type client_id",
input: `{
"private_key_id": "0",
"private_key": "1",
"client_email": "2",
"client_id": 3
}`,
expected: "client_id: Invalid type. Expected: string, given: integer\n",
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := ValidateServiceAccountCredentials(tc.input)
if tc.expected == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tc.expected)
}
})
}
}

0 comments on commit 430d433

Please sign in to comment.