diff --git a/kubedb/client.go b/kubedb/client.go new file mode 100644 index 00000000..601eb33e --- /dev/null +++ b/kubedb/client.go @@ -0,0 +1,202 @@ +/* +Copyright AppsCode Inc. and Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package kubedb is a thin, dependency-free client for the KubeDB Platform API Server, +// exposing the subset of endpoints needed to provision databases in Helm editor mode. +// It is shared by the BMC Helix and ServiceNow provisioning adapters. +package kubedb + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "strings" + "time" +) + +const defaultUserAgent = "bytebuilders-client-go" + +// Client talks to the KubeDB Platform API using a service-account bearer token. +type Client struct { + baseURL string + token string + userAgent string + http *http.Client +} + +// NewClient builds a KubeDB Platform API client. userAgent identifies the caller +// (e.g. "bmchelix-adapter"); an empty value falls back to a default. +func NewClient(baseURL, token, userAgent string, timeout time.Duration, insecureSkipTLS bool) *Client { + if userAgent == "" { + userAgent = defaultUserAgent + } + transport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipTLS}, //nolint:gosec // opt-in dev/test only + DialContext: (&net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}).DialContext, + TLSHandshakeTimeout: 10 * time.Second, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + } + return &Client{ + baseURL: strings.TrimRight(baseURL, "/"), + token: token, + userAgent: userAgent, + http: &http.Client{ + Timeout: timeout, + Transport: transport, + }, + } +} + +// APIError is returned for non-2xx responses from the KubeDB API. +type APIError struct { + Method string + URL string + StatusCode int + Body string +} + +func (e *APIError) Error() string { + return fmt.Sprintf("kubedb api %s %s: status %d: %s", e.Method, e.URL, e.StatusCode, truncate(e.Body, 512)) +} + +// NotFound reports whether the error is a 404 from the KubeDB API. +func (e *APIError) NotFound() bool { return e.StatusCode == http.StatusNotFound } + +// GenerateModel calls PUT /helm/options/model, expanding the given options into a +// full editor model (returned opaquely). +func (c *Client) GenerateModel(ctx context.Context, owner, cluster string, req OptionsModelRequest) (EditorModel, error) { + var model json.RawMessage + if err := c.do(ctx, http.MethodPut, c.clusterPath(owner, cluster, "/helm/options/model"), req, &model); err != nil { + return nil, err + } + return EditorModel(model), nil +} + +// ApplyEditor calls PUT /helm/editor/, submitting the model as an async apply task. +func (c *Client) ApplyEditor(ctx context.Context, owner, cluster string, model EditorModel) (*TaskResponse, error) { + var task TaskResponse + if err := c.do(ctx, http.MethodPut, c.clusterPath(owner, cluster, "/helm/editor/"), model, &task); err != nil { + return nil, err + } + return &task, nil +} + +// DeleteEditor calls DELETE /helm/editor/, removing the release and companion objects. +func (c *Client) DeleteEditor(ctx context.Context, owner, cluster string, meta OptionsMetadata) (*TaskResponse, error) { + var task TaskResponse + if err := c.do(ctx, http.MethodDelete, c.clusterPath(owner, cluster, "/helm/editor/"), DeleteEditorRequest{Metadata: meta}, &task); err != nil { + return nil, err + } + return &task, nil +} + +// GetResource reads a namespaced CR through the Kubernetes proxy. +func (c *Client) GetResource(ctx context.Context, owner, cluster, group, version, resource, namespace, name string) (*Unstructured, error) { + suffix := fmt.Sprintf("/proxy/%s/%s/namespaces/%s/%s/%s", group, version, namespace, resource, name) + var obj Unstructured + if err := c.do(ctx, http.MethodGet, c.clusterPath(owner, cluster, suffix), nil, &obj); err != nil { + return nil, err + } + return &obj, nil +} + +// GetSecret reads a namespaced core/v1 Secret through the proxy and returns its +// decoded (base64) string values. +func (c *Client) GetSecret(ctx context.Context, owner, cluster, namespace, name string) (map[string]string, error) { + suffix := fmt.Sprintf("/proxy/core/v1/namespaces/%s/secrets/%s", namespace, name) + var s Secret + if err := c.do(ctx, http.MethodGet, c.clusterPath(owner, cluster, suffix), nil, &s); err != nil { + return nil, err + } + out := make(map[string]string, len(s.Data)) + for k, v := range s.Data { + if dec, err := base64.StdEncoding.DecodeString(v); err == nil { + out[k] = string(dec) + } else { + out[k] = v + } + } + return out, nil +} + +// AvailableTypes returns the raw JSON of GET /available-types for a cluster. +func (c *Client) AvailableTypes(ctx context.Context, owner, cluster string) (json.RawMessage, error) { + var raw json.RawMessage + if err := c.do(ctx, http.MethodGet, c.clusterPath(owner, cluster, "/available-types"), nil, &raw); err != nil { + return nil, err + } + return raw, nil +} + +func (c *Client) clusterPath(owner, cluster, suffix string) string { + return fmt.Sprintf("%s/api/v1/clusters/%s/%s%s", c.baseURL, owner, cluster, suffix) +} + +func (c *Client) do(ctx context.Context, method, url string, body, out any) error { + var reqBody io.Reader + if body != nil { + b, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("marshal request: %w", err) + } + reqBody = bytes.NewReader(b) + } + + req, err := http.NewRequestWithContext(ctx, method, url, reqBody) + if err != nil { + return err + } + req.Header.Set("Authorization", "token "+c.token) + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", c.userAgent) + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + + resp, err := c.http.Do(req) + if err != nil { + return fmt.Errorf("%s %s: %w", method, url, err) + } + defer resp.Body.Close() + + data, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20)) + if err != nil { + return fmt.Errorf("read response: %w", err) + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return &APIError{Method: method, URL: url, StatusCode: resp.StatusCode, Body: string(data)} + } + if out != nil && len(data) > 0 { + if err := json.Unmarshal(data, out); err != nil { + return fmt.Errorf("decode response: %w", err) + } + } + return nil +} + +func truncate(s string, n int) string { + if len(s) <= n { + return s + } + return s[:n] + "…" +} diff --git a/kubedb/client_test.go b/kubedb/client_test.go new file mode 100644 index 00000000..d39c0bde --- /dev/null +++ b/kubedb/client_test.go @@ -0,0 +1,138 @@ +/* +Copyright AppsCode Inc. and Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubedb + +import ( + "context" + "encoding/base64" + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +const testUA = "unit-test-agent" + +func newTestClient(t *testing.T, h http.Handler) *Client { + t.Helper() + srv := httptest.NewServer(h) + t.Cleanup(srv.Close) + return NewClient(srv.URL, "tok", testUA, 5*time.Second, false) +} + +func TestNewClient_DefaultUserAgent(t *testing.T) { + c := NewClient("https://example.com", "tok", "", time.Second, false) + if c.userAgent != defaultUserAgent { + t.Fatalf("userAgent = %q, want default %q", c.userAgent, defaultUserAgent) + } +} + +func TestGetResource_ParsesStatusAndSendsHeaders(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("GET /api/v1/clusters/acme/prod/proxy/kubedb.com/v1/namespaces/team-a/postgreses/db1", + func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("Authorization"); got != "token tok" { + t.Errorf("Authorization = %q, want %q", got, "token tok") + } + if got := r.Header.Get("User-Agent"); got != testUA { + t.Errorf("User-Agent = %q, want %q", got, testUA) + } + _, _ = w.Write([]byte(`{"status":{"phase":"Ready"}}`)) + }) + + c := newTestClient(t, mux) + obj, err := c.GetResource(context.Background(), "acme", "prod", "kubedb.com", "v1", "postgreses", "team-a", "db1") + if err != nil { + t.Fatalf("GetResource: %v", err) + } + if obj.Status.Phase != "Ready" { + t.Fatalf("phase = %q, want Ready", obj.Status.Phase) + } +} + +func TestGetResource_NotFound(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("GET /api/v1/clusters/acme/prod/proxy/kubedb.com/v1/namespaces/team-a/postgreses/missing", + func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message":"not found"}`)) + }) + + c := newTestClient(t, mux) + _, err := c.GetResource(context.Background(), "acme", "prod", "kubedb.com", "v1", "postgreses", "team-a", "missing") + var apiErr *APIError + if !errors.As(err, &apiErr) || !apiErr.NotFound() { + t.Fatalf("want APIError NotFound, got %v", err) + } +} + +func TestGetSecret_DecodesBase64(t *testing.T) { + enc := base64.StdEncoding.EncodeToString + mux := http.NewServeMux() + mux.HandleFunc("GET /api/v1/clusters/acme/prod/proxy/core/v1/namespaces/team-a/secrets/db1-auth", + func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(`{"data":{"username":"` + enc([]byte("pg-user")) + `","password":"` + enc([]byte("p@ss")) + `"}}`)) + }) + + c := newTestClient(t, mux) + creds, err := c.GetSecret(context.Background(), "acme", "prod", "team-a", "db1-auth") + if err != nil { + t.Fatalf("GetSecret: %v", err) + } + if creds["username"] != "pg-user" || creds["password"] != "p@ss" { + t.Fatalf("decoded creds = %v", creds) + } +} + +func TestGenerateModel_APIErrorStatus(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("PUT /api/v1/clusters/acme/prod/helm/options/model", + func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnprocessableEntity) + _, _ = w.Write([]byte(`{"message":"bad options"}`)) + }) + + c := newTestClient(t, mux) + _, err := c.GenerateModel(context.Background(), "acme", "prod", OptionsModelRequest{}) + var apiErr *APIError + if !errors.As(err, &apiErr) || apiErr.StatusCode != http.StatusUnprocessableEntity { + t.Fatalf("want 422 APIError, got %v", err) + } +} + +func TestApplyEditor_SendsModelAndReturnsTask(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("PUT /api/v1/clusters/acme/prod/helm/editor/", + func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + if string(body) != `{"k":"v"}` { + t.Errorf("editor body = %q, want the model verbatim", string(body)) + } + _, _ = w.Write([]byte(`{"id":"task-1"}`)) + }) + + c := newTestClient(t, mux) + task, err := c.ApplyEditor(context.Background(), "acme", "prod", EditorModel(`{"k":"v"}`)) + if err != nil { + t.Fatalf("ApplyEditor: %v", err) + } + if task.ID != "task-1" { + t.Fatalf("task id = %q", task.ID) + } +} diff --git a/kubedb/engines.go b/kubedb/engines.go new file mode 100644 index 00000000..c293027a --- /dev/null +++ b/kubedb/engines.go @@ -0,0 +1,102 @@ +/* +Copyright AppsCode Inc. and Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubedb + +import ( + "fmt" + "sort" + "strings" +) + +// Engine describes the KubeDB coordinates for a single database kind. Values here +// are derived from kubedb.dev/apimachinery (apis/kubedb): a kind served under the GA +// group version uses "v1"; the remaining kinds use "v1alpha2". Confirm availability +// for a given cluster via `available-types`. +type Engine struct { + // Name is the canonical lower-case engine name, e.g. "postgres". + Name string + // Group/Version/Kind/Resource identify the primary CR for the proxy path. + Group string + Version string + Kind string + Resource string + // Ports are the engine's default client service ports (informational). + Ports []int + // AuthType hints how connection credentials in the {name}-auth secret are shaped: + // "password" (username/password) or "apikey" (Qdrant/Weaviate). + AuthType string +} + +const group = "kubedb.com" + +// engines is the full set of databases KubeDB supports. Kinds served under the GA +// group version use "v1"; the rest use "v1alpha2". +var engines = map[string]Engine{ + // --- kubedb.com/v1 (GA) --- + "elasticsearch": {Name: "elasticsearch", Group: group, Version: "v1", Kind: "Elasticsearch", Resource: "elasticsearches", Ports: []int{9200}, AuthType: "password"}, + "kafka": {Name: "kafka", Group: group, Version: "v1", Kind: "Kafka", Resource: "kafkas", Ports: []int{9092}, AuthType: "password"}, + "mariadb": {Name: "mariadb", Group: group, Version: "v1", Kind: "MariaDB", Resource: "mariadbs", Ports: []int{3306}, AuthType: "password"}, + "memcached": {Name: "memcached", Group: group, Version: "v1", Kind: "Memcached", Resource: "memcacheds", Ports: []int{11211}, AuthType: "password"}, + "mongodb": {Name: "mongodb", Group: group, Version: "v1", Kind: "MongoDB", Resource: "mongodbs", Ports: []int{27017}, AuthType: "password"}, + "mysql": {Name: "mysql", Group: group, Version: "v1", Kind: "MySQL", Resource: "mysqls", Ports: []int{3306}, AuthType: "password"}, + "perconaxtradb": {Name: "perconaxtradb", Group: group, Version: "v1", Kind: "PerconaXtraDB", Resource: "perconaxtradbs", Ports: []int{3306}, AuthType: "password"}, + "pgbouncer": {Name: "pgbouncer", Group: group, Version: "v1", Kind: "PgBouncer", Resource: "pgbouncers", Ports: []int{5432}, AuthType: "password"}, + "postgres": {Name: "postgres", Group: group, Version: "v1", Kind: "Postgres", Resource: "postgreses", Ports: []int{5432}, AuthType: "password"}, + "proxysql": {Name: "proxysql", Group: group, Version: "v1", Kind: "ProxySQL", Resource: "proxysqls", Ports: []int{6033}, AuthType: "password"}, + "redis": {Name: "redis", Group: group, Version: "v1", Kind: "Redis", Resource: "redises", Ports: []int{6379}, AuthType: "password"}, + "redissentinel": {Name: "redissentinel", Group: group, Version: "v1", Kind: "RedisSentinel", Resource: "redissentinels", Ports: []int{26379}, AuthType: "password"}, + + // --- kubedb.com/v1alpha2 --- + "aerospike": {Name: "aerospike", Group: group, Version: "v1alpha2", Kind: "Aerospike", Resource: "aerospikes", Ports: []int{3000}, AuthType: "password"}, + "cassandra": {Name: "cassandra", Group: group, Version: "v1alpha2", Kind: "Cassandra", Resource: "cassandras", Ports: []int{9042}, AuthType: "password"}, + "clickhouse": {Name: "clickhouse", Group: group, Version: "v1alpha2", Kind: "ClickHouse", Resource: "clickhouses", Ports: []int{9000, 8123}, AuthType: "password"}, + "documentdb": {Name: "documentdb", Group: group, Version: "v1alpha2", Kind: "DocumentDB", Resource: "documentdbs", Ports: []int{10260}, AuthType: "password"}, + "druid": {Name: "druid", Group: group, Version: "v1alpha2", Kind: "Druid", Resource: "druids", Ports: []int{8888}, AuthType: "password"}, + "hanadb": {Name: "hanadb", Group: group, Version: "v1alpha2", Kind: "HanaDB", Resource: "hanadbs", Ports: []int{39017}, AuthType: "password"}, + "hazelcast": {Name: "hazelcast", Group: group, Version: "v1alpha2", Kind: "Hazelcast", Resource: "hazelcasts", Ports: []int{5701}, AuthType: "password"}, + "ignite": {Name: "ignite", Group: group, Version: "v1alpha2", Kind: "Ignite", Resource: "ignites", Ports: []int{10800}, AuthType: "password"}, + "milvus": {Name: "milvus", Group: group, Version: "v1alpha2", Kind: "Milvus", Resource: "milvuses", Ports: []int{19530}, AuthType: "password"}, + "mssqlserver": {Name: "mssqlserver", Group: group, Version: "v1alpha2", Kind: "MSSQLServer", Resource: "mssqlservers", Ports: []int{1433}, AuthType: "password"}, + "oracle": {Name: "oracle", Group: group, Version: "v1alpha2", Kind: "Oracle", Resource: "oracles", Ports: []int{1521}, AuthType: "password"}, + "pgpool": {Name: "pgpool", Group: group, Version: "v1alpha2", Kind: "Pgpool", Resource: "pgpools", Ports: []int{9999}, AuthType: "password"}, + "qdrant": {Name: "qdrant", Group: group, Version: "v1alpha2", Kind: "Qdrant", Resource: "qdrants", Ports: []int{6333, 6334}, AuthType: "apikey"}, + "rabbitmq": {Name: "rabbitmq", Group: group, Version: "v1alpha2", Kind: "RabbitMQ", Resource: "rabbitmqs", Ports: []int{5672, 15672}, AuthType: "password"}, + "singlestore": {Name: "singlestore", Group: group, Version: "v1alpha2", Kind: "Singlestore", Resource: "singlestores", Ports: []int{3306}, AuthType: "password"}, + "solr": {Name: "solr", Group: group, Version: "v1alpha2", Kind: "Solr", Resource: "solrs", Ports: []int{8983}, AuthType: "password"}, + "weaviate": {Name: "weaviate", Group: group, Version: "v1alpha2", Kind: "Weaviate", Resource: "weaviates", Ports: []int{8080, 50051}, AuthType: "apikey"}, + "zookeeper": {Name: "zookeeper", Group: group, Version: "v1alpha2", Kind: "ZooKeeper", Resource: "zookeepers", Ports: []int{2181}, AuthType: "password"}, +} + +// Lookup returns the Engine for a name (case-insensitive) or an error listing +// the supported engines. +func Lookup(name string) (Engine, error) { + e, ok := engines[strings.ToLower(strings.TrimSpace(name))] + if !ok { + return Engine{}, fmt.Errorf("unsupported engine %q (supported: %s)", name, strings.Join(SupportedEngines(), ", ")) + } + return e, nil +} + +// SupportedEngines returns the sorted list of engine names the client knows about. +func SupportedEngines() []string { + out := make([]string, 0, len(engines)) + for k := range engines { + out = append(out, k) + } + sort.Strings(out) + return out +} diff --git a/kubedb/engines_test.go b/kubedb/engines_test.go new file mode 100644 index 00000000..eca4c786 --- /dev/null +++ b/kubedb/engines_test.go @@ -0,0 +1,88 @@ +/* +Copyright AppsCode Inc. and Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubedb + +import "testing" + +func TestEngines_Complete(t *testing.T) { + if got := len(SupportedEngines()); got != 30 { + t.Fatalf("expected 30 supported engines, got %d", got) + } + for _, name := range SupportedEngines() { + e, err := Lookup(name) + if err != nil { + t.Fatalf("Lookup(%q): %v", name, err) + } + if e.Group != "kubedb.com" { + t.Errorf("%s: group = %q, want kubedb.com", name, e.Group) + } + if e.Version != "v1" && e.Version != "v1alpha2" { + t.Errorf("%s: version = %q, want v1 or v1alpha2", name, e.Version) + } + if e.Kind == "" || e.Resource == "" { + t.Errorf("%s: missing kind/resource: %+v", name, e) + } + if len(e.Ports) == 0 { + t.Errorf("%s: no ports", name) + } + } +} + +func TestEngines_KnownCoordinates(t *testing.T) { + // Spot-check a sample against kubedb.dev/apimachinery to catch registry typos. + want := map[string]struct { + version, kind, resource, auth string + }{ + "postgres": {"v1", "Postgres", "postgreses", "password"}, + "redis": {"v1", "Redis", "redises", "password"}, + "mysql": {"v1", "MySQL", "mysqls", "password"}, + "memcached": {"v1", "Memcached", "memcacheds", "password"}, // has auth by default + "mssqlserver": {"v1alpha2", "MSSQLServer", "mssqlservers", "password"}, + "qdrant": {"v1alpha2", "Qdrant", "qdrants", "apikey"}, + "weaviate": {"v1alpha2", "Weaviate", "weaviates", "apikey"}, + "milvus": {"v1alpha2", "Milvus", "milvuses", "password"}, // no API key + "perconaxtradb": {"v1", "PerconaXtraDB", "perconaxtradbs", "password"}, + } + for name, w := range want { + e, err := Lookup(name) + if err != nil { + t.Fatalf("Lookup(%q): %v", name, err) + } + if e.Version != w.version || e.Kind != w.kind || e.Resource != w.resource || e.AuthType != w.auth { + t.Errorf("%s = {%s %s %s %s}, want {%s %s %s %s}", + name, e.Version, e.Kind, e.Resource, e.AuthType, w.version, w.kind, w.resource, w.auth) + } + } +} + +func TestEngines_AuthTypeValues(t *testing.T) { + for _, name := range SupportedEngines() { + e, _ := Lookup(name) + if e.AuthType != "password" && e.AuthType != "apikey" { + t.Errorf("%s: unexpected AuthType %q", name, e.AuthType) + } + } +} + +func TestLookup_CaseInsensitiveAndUnknown(t *testing.T) { + if e, err := Lookup("PostGres"); err != nil || e.Kind != "Postgres" { + t.Fatalf("case-insensitive lookup failed: %+v, %v", e, err) + } + if _, err := Lookup("cockroach"); err == nil { + t.Fatalf("expected error for unknown engine") + } +} diff --git a/kubedb/types.go b/kubedb/types.go new file mode 100644 index 00000000..d8a2bf90 --- /dev/null +++ b/kubedb/types.go @@ -0,0 +1,88 @@ +/* +Copyright AppsCode Inc. and Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubedb + +import "encoding/json" + +// OptionsModelRequest is the body for PUT /helm/options/model — the small set of +// options the KubeDB editor expands into a full CR + companion objects. +type OptionsModelRequest struct { + Metadata OptionsMetadata `json:"metadata"` + Spec map[string]any `json:"spec,omitempty"` +} + +// OptionsMetadata identifies the target resource kind and release for the editor. +type OptionsMetadata struct { + Resource ResourceRef `json:"resource"` + Release ReleaseRef `json:"release"` +} + +// ResourceRef is a KubeDB group/version/kind reference. +type ResourceRef struct { + Group string `json:"group"` + Version string `json:"version"` + Kind string `json:"kind"` +} + +// ReleaseRef names the installation (database) the editor operates on. +type ReleaseRef struct { + Name string `json:"name"` + Namespace string `json:"namespace"` +} + +// EditorModel is the opaque model returned by options/model and consumed, unchanged, +// by the editor apply call. +type EditorModel = json.RawMessage + +// DeleteEditorRequest is the body for DELETE /helm/editor/. +type DeleteEditorRequest struct { + Metadata OptionsMetadata `json:"metadata"` +} + +// TaskResponse is the async acknowledgement returned by editor apply/delete. +type TaskResponse struct { + ID string `json:"id,omitempty"` + Task string `json:"task,omitempty"` + Status string `json:"status,omitempty"` +} + +// Unstructured is a minimal view of a Kubernetes object — enough to read status and +// identify it. The full payload is not retained. +type Unstructured struct { + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Metadata ObjectMeta `json:"metadata"` + Status ResourceStatus `json:"status"` +} + +// ObjectMeta is a minimal Kubernetes object metadata view. +type ObjectMeta struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + UID string `json:"uid"` + Labels map[string]string `json:"labels,omitempty"` +} + +// ResourceStatus captures the KubeDB database phase (e.g. "Ready", "Provisioning"). +type ResourceStatus struct { + Phase string `json:"phase"` +} + +// Secret is a minimal core/v1 Secret view (base64-encoded data values). +type Secret struct { + Data map[string]string `json:"data"` +}