diff --git a/Taskfile.yml b/Taskfile.yml index b91e112..1bbed40 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -49,6 +49,7 @@ tasks: ## Testing fmt: cmds: + - go fmt ./... - "{{.LOCAL_BIN}}/golangci-lint fmt ./..." lint: deps: [setup:golangci-lint] diff --git a/cmd/listener.go b/cmd/listener.go index 21599ba..1e42226 100644 --- a/cmd/listener.go +++ b/cmd/listener.go @@ -28,8 +28,7 @@ import ( ) var listenCmd = &cobra.Command{ - Use: "listener", - Example: "KUBECONFIG= go run . listener", + Use: "listener", Run: func(cmd *cobra.Command, args []string) { log.Info().Str("LogLevel", log.GetLevel().String()).Msg("Starting the Listener...") diff --git a/common/auth/config.go b/common/auth/config.go index c67903b..0ebe374 100644 --- a/common/auth/config.go +++ b/common/auth/config.go @@ -5,27 +5,24 @@ import ( "encoding/base64" "errors" "fmt" + "time" gatewayv1alpha1 "github.com/platform-mesh/kubernetes-graphql-gateway/common/apis/v1alpha1" authv1 "k8s.io/api/authentication/v1" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" ) // BuildConfig creates a rest.Config from cluster connection parameters // This function unifies the authentication logic used by both listener and gateway -func BuildConfig(ctx context.Context, host string, auth *gatewayv1alpha1.AuthConfig, ca *gatewayv1alpha1.CAConfig, k8sClient client.Client) (*rest.Config, error) { - if host == "" { - return nil, errors.New("host is required") - } - +func BuildConfig(ctx context.Context, auth *gatewayv1alpha1.AuthConfig, ca *gatewayv1alpha1.CAConfig, k8sClient client.Client) (*rest.Config, error) { config := &rest.Config{ - Host: host, TLSClientConfig: rest.TLSClientConfig{ Insecure: true, // Start with insecure, will be overridden if CA is provided }, @@ -61,7 +58,6 @@ func BuildConfigFromMetadata(host string, authType, token, kubeconfig, certData, } config := &rest.Config{ - Host: host, TLSClientConfig: rest.TLSClientConfig{ Insecure: true, // Start with insecure, will be overridden if CA is provided }, @@ -113,6 +109,8 @@ func BuildConfigFromMetadata(host string, authType, token, kubeconfig, certData, } } + config.Host = host + return config, nil } @@ -252,36 +250,28 @@ func ConfigureAuthentication(ctx context.Context, config *rest.Config, auth *gat } if auth.ServiceAccount != nil { - var expirationSeconds int64 + expiration := 1 * time.Hour if auth.ServiceAccount.TokenExpiration != nil { - // If TokenExpiration is provided, use its value - expirationSeconds = int64(auth.ServiceAccount.TokenExpiration.Seconds()) - } else { - // If TokenExpiration is nil, use the desired default (3600 seconds = 1 hour) - expirationSeconds = 3600 - fmt.Println("Warning: auth.ServiceAccount.TokenExpiration is nil, defaulting to 3600 seconds.") + expiration = auth.ServiceAccount.TokenExpiration.Duration } // Build the TokenRequest object tokenRequest := &authv1.TokenRequest{ Spec: authv1.TokenRequestSpec{ - Audiences: auth.ServiceAccount.Audience, - // Optionally set ExpirationSeconds, BoundObjectRef, etc. - ExpirationSeconds: &expirationSeconds, + Audiences: auth.ServiceAccount.Audience, + ExpirationSeconds: ptr.To(int64(expiration.Seconds())), }, } // Get the service account token using the Kubernetes API - sa := &corev1.ServiceAccount{} - err := k8sClient.Get(ctx, types.NamespacedName{ - Name: auth.ServiceAccount.Name, - Namespace: auth.ServiceAccount.Namespace, - }, sa) - if err != nil { - return errors.Join(errors.New("failed to get service account"), err) + sa := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: auth.ServiceAccount.Name, + Namespace: auth.ServiceAccount.Namespace, + }, } - err = k8sClient.SubResource("token").Create(ctx, sa, tokenRequest) + err := k8sClient.SubResource("token").Create(ctx, sa, tokenRequest) if err != nil { return errors.Join(errors.New("failed to create token request for service account"), err) } @@ -306,62 +296,12 @@ func ConfigureFromKubeconfig(config *rest.Config, kubeconfigData []byte) error { return errors.Join(errors.New("failed to parse kubeconfig"), err) } - rawConfig, err := clientConfig.RawConfig() + extracted, err := clientConfig.ClientConfig() if err != nil { return errors.Join(errors.New("failed to get raw kubeconfig"), err) } - // Get the current context - currentContext := rawConfig.CurrentContext - if currentContext == "" { - return errors.New("no current context in kubeconfig") - } - - context, exists := rawConfig.Contexts[currentContext] - if !exists { - return errors.New("current context not found in kubeconfig") - } - - // Get auth info for current context - authInfo, exists := rawConfig.AuthInfos[context.AuthInfo] - if !exists { - return errors.New("auth info not found in kubeconfig") - } - - // Extract authentication information - return ExtractAuthFromKubeconfig(config, authInfo) -} - -// ExtractAuthFromKubeconfig extracts authentication info from kubeconfig AuthInfo -func ExtractAuthFromKubeconfig(config *rest.Config, authInfo *api.AuthInfo) error { - if authInfo.Token != "" { - config.BearerToken = authInfo.Token - return nil - } - - if authInfo.TokenFile != "" { - // TODO: Read token from file if needed - return errors.New("token file authentication not yet implemented") - } - - if len(authInfo.ClientCertificateData) > 0 && len(authInfo.ClientKeyData) > 0 { - config.CertData = authInfo.ClientCertificateData - config.KeyData = authInfo.ClientKeyData - return nil - } + *config = *extracted - if authInfo.ClientCertificate != "" && authInfo.ClientKey != "" { - config.CertFile = authInfo.ClientCertificate - config.KeyFile = authInfo.ClientKey - return nil - } - - if authInfo.Username != "" && authInfo.Password != "" { - config.Username = authInfo.Username - config.Password = authInfo.Password - return nil - } - - // No recognizable authentication found - return errors.New("no valid authentication method found in kubeconfig") + return nil } diff --git a/common/auth/config_test.go b/common/auth/config_test.go index d280264..a38413f 100644 --- a/common/auth/config_test.go +++ b/common/auth/config_test.go @@ -13,7 +13,6 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd/api" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -271,112 +270,3 @@ clusters: }) } } - -func TestExtractAuthFromKubeconfig(t *testing.T) { - tests := []struct { - name string - authInfo *api.AuthInfo - wantConfig func(*rest.Config) *rest.Config - wantErr bool - errContains string - }{ - { - name: "token_auth", - authInfo: &api.AuthInfo{ - Token: "test-token", - }, - wantConfig: func(config *rest.Config) *rest.Config { - expected := *config - expected.BearerToken = "test-token" - return &expected - }, - wantErr: false, - }, - { - name: "client_certificate_data", - authInfo: &api.AuthInfo{ - ClientCertificateData: []byte("cert-data"), - ClientKeyData: []byte("key-data"), - }, - wantConfig: func(config *rest.Config) *rest.Config { - expected := *config - expected.CertData = []byte("cert-data") - expected.KeyData = []byte("key-data") - return &expected - }, - wantErr: false, - }, - { - name: "client_certificate_files", - authInfo: &api.AuthInfo{ - ClientCertificate: "/path/to/cert.pem", - ClientKey: "/path/to/key.pem", - }, - wantConfig: func(config *rest.Config) *rest.Config { - expected := *config - expected.CertFile = "/path/to/cert.pem" - expected.KeyFile = "/path/to/key.pem" - return &expected - }, - wantErr: false, - }, - { - name: "basic_auth", - authInfo: &api.AuthInfo{ - Username: "test-user", - Password: "test-password", - }, - wantConfig: func(config *rest.Config) *rest.Config { - expected := *config - expected.Username = "test-user" - expected.Password = "test-password" - return &expected - }, - wantErr: false, - }, - { - name: "token_file_not_implemented", - authInfo: &api.AuthInfo{ - TokenFile: "/path/to/token", - }, - wantConfig: func(config *rest.Config) *rest.Config { - return config - }, - wantErr: true, - errContains: "token file authentication not yet implemented", - }, - { - name: "no_auth_info", - authInfo: &api.AuthInfo{}, - wantConfig: func(config *rest.Config) *rest.Config { - return config - }, - wantErr: true, - errContains: "no valid authentication method found in kubeconfig", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - config := &rest.Config{ - Host: "https://test.example.com", - TLSClientConfig: rest.TLSClientConfig{ - Insecure: true, - }, - } - - err := ExtractAuthFromKubeconfig(config, tt.authInfo) - - if tt.wantErr { - assert.Error(t, err) - if tt.errContains != "" { - assert.Contains(t, err.Error(), tt.errContains) - } - } else { - assert.NoError(t, err) - expected := tt.wantConfig(config) - assert.Equal(t, expected, config) - } - }) - } -} diff --git a/docs/assets/Listener_High_Level.drawio.svg b/docs/assets/Listener_High_Level.drawio.svg deleted file mode 100644 index b718bf3..0000000 --- a/docs/assets/Listener_High_Level.drawio.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -
APIServer
APIServer
apiVersion: apis.kcp.io/v1alpha1
kind: APIBinding
metadata:
name: core.openmfp.io
spec:
reference:
export:
name: core.openmfp.io
path: root
apiVersion: apis.kcp.io/v1alpha1...
Listener
Listener
APISchemaResolver
APISchemaResolver
Uses
Uses
Uses
Uses
APIBindingReconciler
APIBindingReconciler
IOHandler
IOHandler
Uses
Uses
CRDReconciler
CRDReconciler
Uses
Uses
Reconciles
Reconciles
Resolves
Resolves
{
  "definitions": {
    "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": {
      "description": "...",
      "type": "object"
    },
{...
JSON File
JSON File
Queries
Queries
Writes
Writes
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: issuers.cert-manager.io
spec:
group: cert-manager.io
names:
kind: Issuer
listKind: IssuerList
plural: issuers
singular: issuer
categories:
- cert-manager
scope: Namespaced
versions:
- name: v1
......
apiVersion: apiextensions.k8s.io/v1...
Gateway
Gateway
Loads
Loads
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/authorization.md b/docs/authorization.md deleted file mode 100644 index d069072..0000000 --- a/docs/authorization.md +++ /dev/null @@ -1,34 +0,0 @@ -# Authorization - -All requests must contain an `Authorization` header with a valid Bearer token by default: -```shell -{ - "Authorization": "Bearer $YOUR_TOKEN" -} -``` -You can disable authorization by setting the following environment variable: -```shell -export LOCAL_DEVELOPMENT=true -``` -This is useful for local development and testing purposes. - -## Introspection authentication - -By default, introspection requests (i.e., the requests that are made to fetch the GraphQL schema) are **not** protected by authorization. - -You can protect those requests by setting the following environment variable: -```shell -export GATEWAY_INTROSPECTION_AUTHENTICATION=true -``` - -### Error fetching schema in documentation explorer - -When the GraphiQL page is loaded, it makes a request to fetch the GraphQL schema, and there is no way to add the `Authorization` header to that request. - -We have this [issue](https://github.com/openmfp/kubernetes-graphql-gateway/issues/217) open to fix this. - -But for now, you can use the following workaround: -1. Open the GraphiQL page in your browser. -2. Add the `Authorization` header in the `Headers` section of the GraphiQL user interface. -3. Press the `Re-fetch GraphQL schema` button in the left sidebar (third button from the top). -4. The GraphQL schema should now be fetched, and you can use the GraphiQL interface as usual. diff --git a/docs/clusteraccess-setup.md b/docs/clusteraccess-setup.md deleted file mode 100644 index e39cfed..0000000 --- a/docs/clusteraccess-setup.md +++ /dev/null @@ -1,184 +0,0 @@ -# ClusterAccess Resource Setup - -To enable the gateway to access external Kubernetes clusters, you need to create ClusterAccess resources. This section provides both an automated script and manual step-by-step instructions. - -## Quick Setup (Recommended) - -For development purposes, use the provided script to automatically create ClusterAccess resources: - -**Example:** -```bash -./hack/create-clusteraccess.sh --target-kubeconfig ~/.kube/platform-mesh-config --management-kubeconfig ~/.kube/platform-mesh-config -``` - -The script will: -- Extract cluster name, server URL, and CA certificate from the target kubeconfig -- Create a ServiceAccount with cluster-admin access in the target cluster -- Generate a long-lived token for the ServiceAccount -- Create the admin kubeconfig and CA secrets in the management cluster -- Create the ClusterAccess resource with kubeconfig-based authentication -- Output a copy-paste ready bearer token for direct API access - -## Manual Setup - -## Prerequisites - -- Access to the target cluster (the cluster you want to expose via GraphQL) -- Access to the management cluster (the cluster where the gateway runs) -- ClusterAccess CRDs installed in the management cluster -- Target cluster kubeconfig file - -## Step 1: Create ServiceAccount with Admin Access in Target Cluster - -```bash -# Switch to target cluster -export KUBECONFIG=/path/to/target-cluster-kubeconfig - -# Create ServiceAccount with cluster-admin access -cat </graphql` which will be used to query the resources of that workspace or cluster. -It watches for changes in the directory and updates the schema accordingly. - -So, if there are two files in the directory - `root` and `root:alpha`, then we will have two URLs: -- `http://localhost:3000/root/graphql` -- `http://localhost:3000/root:alpha/graphql` - -Open the URL in the browser and you will see the GraphQL playground. - -See example queries in the [Queries Examples](./quickstart.md#first-steps-and-basic-examples) section. - -## Packages Overview - -### Manager (`gateway/manager/`) - -Manages the gateway lifecycle and cluster connections: -- **Watcher**: Watches the definitions directory for schema file changes -- **Target Cluster**: Manages cluster registry and GraphQL endpoint routing -- **Round Tripper**: Handles HTTP request routing and authentication - -### Schema (`gateway/schema/`) - -Converts OpenAPI specifications into GraphQL schemas: -- Generates GraphQL types from OpenAPI definitions -- Handles custom queries and relations between resources -- Manages scalar type mappings - -### Resolver (`gateway/resolver/`) - -Executes GraphQL queries against Kubernetes clusters: -- Resolves GraphQL queries to Kubernetes API calls -- Handles subscriptions for real-time updates -- Processes query arguments and filters diff --git a/docs/listener.md b/docs/listener.md deleted file mode 100644 index 761bd0f..0000000 --- a/docs/listener.md +++ /dev/null @@ -1,40 +0,0 @@ -# Listener - -The Listener component is responsible for watching Kubernetes clusters and generating OpenAPI specifications for discovered resources. -It stores these specifications in a directory, which can then be used by the [Gateway](./gateway.md) component to expose them as GraphQL endpoints. - -In **KCP mode**, it creates a separate file for each KCP workspace. In **ClusterAccess mode**, it creates a file for each ClusterAccess resource representing a target cluster. - -The Gateway watches this directory for changes and updates the GraphQL schema accordingly. - -## Packages Overview - -### Reconciler (`listener/reconciler/`) - -Contains reconciliation logic for different operational modes: - -#### ClusterAccess Reconciler (`reconciler/clusteraccess/`) -- Watches ClusterAccess resources in the management cluster -- Connects to target clusters using kubeconfig-based authentication -- Generates schema files with embedded cluster connection metadata -- Injects `x-cluster-metadata` into schema files for gateway consumption - -#### KCP Reconciler (`reconciler/kcp/`) -- Watches APIBinding resources in KCP workspaces -- Discovers virtual workspaces and their API resources -- Handles cluster path resolution for KCP workspace hierarchies -- Generates schema files for each workspace - -### Packages (`listener/pkg/`) - -Supporting packages for schema generation: - -#### API Schema (`pkg/apischema/`) -- Builds OpenAPI specifications from Kubernetes API resources -- Resolves Custom Resource Definitions (CRDs) -- Converts Kubernetes API schemas to OpenAPI format -- Handles resource relationships and dependencies - -#### Workspace File (`pkg/workspacefile/`) -- Manages reading and writing schema files to the definitions directory -- Handles file I/O operations for schema persistence diff --git a/docs/local_test.md b/docs/local_test.md deleted file mode 100644 index 957374a..0000000 --- a/docs/local_test.md +++ /dev/null @@ -1,75 +0,0 @@ -# Test Locally - -## Run and check cluster - -1. Create and run a cluster if it is not running yet. - -```shell -git clone https://github.com/platform-mesh/helm-charts.git -cd helm-charts -task local-setup -``` -If this task fails, you can try to run `task local-setup:iterate` to complete it. - -2. Verify that the cluster is running. - -Run k9s and go to `:pods`. All pods should have a status of "Running". -It may take some time before they are all ready. Possible issues include insufficient RAM and/or CPU cores. In this case, increase the limits in Docker settings. - -3. In k9s, go to `:pods`, then open pod `kubernetes-graphql-gateway-...`. - -Open container `kubernetes-graphql-gateway-gateway` to see the logs. -The logs must contain more than a single line (with "Starting server..."). -If you see only this single line, the problem might be in the container called "kubernetes-graphql-gateway-listener". - -Note the image name from one of the `kubernetes-...` containers. It contains the name and the currently used version of the build, e.g.: -``` -ghcr.io/platform-mesh/kubernetes-graphql-gateway:v0.75.1 -``` - -4. Build the Docker image: -```shell -task docker -``` - -5. Tag the newly built image with the version used in local-setup: -```shell -docker tag ghcr.io/platform-mesh/kubernetes-graphql-gateway:latest ghcr.io/platform-mesh/kubernetes-graphql-gateway:v0.75.1 -``` -Use the name and version you got from the `IMAGE` column in step 3. - -6. Check your cluster name: -```shell -kind get clusters -``` -In this example, the cluster name is `platform-mesh`. - -7. Load the new image into your kind cluster: -***Docker-based kind:*** -```shell -kind load docker-image ghcr.io/platform-mesh/kubernetes-graphql-gateway:v0.75.1 -n platform-mesh -``` -The argument `-n platform-mesh` targets the platform-mesh kind cluster. - -***Podman-based kind:*** -- Pull (or build) the image locally with Podman: -```shell -podman pull ghcr.io/platform-mesh/kubernetes-graphql-gateway:v0.75.1 -``` - -- Save it to a tarball in OCI-archive format: -```shell -podman save --format oci-archive ghcr.io/platform-mesh/kubernetes-graphql-gateway:v0.75.1 -o kubernetes-graphql-gateway_v0.75.1.tar -``` - -- Load the tarball into your Podman-backed kind cluster: -```shell -kind load image-archive kubernetes-graphql-gateway_v0.75.1.tar -n platform-mesh -``` - -8. In k9s, go to `:pods` and delete the pod (not the container) called `kubernetes-graphql-gateway-...`. - -Kubernetes will immediately recreate the pod - but this time it will use the new version of the build. - -9. Once the pod is recreated, go to [https://portal.dev.local:8443](https://portal.dev.local:8443) -and check if everything works fine. diff --git a/docs/pod_queries.md b/docs/pod_queries.md deleted file mode 100644 index 6bd9a58..0000000 --- a/docs/pod_queries.md +++ /dev/null @@ -1,89 +0,0 @@ -# Pod Queries and Mutations - -This page shows you examples queries and mutations for GraphQL to perform operations on the `Pod` resource. -For questions on how to execute them, please find our [Quick Start Guide](./quickstart.md). - -## Create a Pod: -```shell -mutation { - core { - createPod( - namespace: "default", - object: { - metadata: { - name: "my-new-pod", - labels: { - app: "my-app" - } - } - spec: { - containers: [ - { - name: "nginx-container" - image: "nginx:latest" - ports: [ - { - containerPort: 80 - } - ] - } - ] - restartPolicy: "Always" - } - } - ) { - metadata { - name - namespace - labels - } - spec { - containers { - name - image - ports { - containerPort - } - } - restartPolicy - } - status { - phase - } - } - } -} -``` - -## Get the Created Pod: -```shell -query { - core { - Pod(name:"my-new-pod", namespace:"default") { - metadata { - name - } - spec{ - containers { - image - ports { - containerPort - } - } - } - } - } -} -``` - -## Delete the Created Pod: -```shell -mutation { - core { - deletePod( - namespace: "default", - name: "my-new-pod" - ) - } -} -``` diff --git a/docs/quickstart.md b/docs/quickstart.md deleted file mode 100644 index 2987e67..0000000 --- a/docs/quickstart.md +++ /dev/null @@ -1,106 +0,0 @@ -# Quick Start - -This page shows you how to get started using the GraphQL Gateway for Kubernetes. - -## Prerequisites -- Installed [Golang](https://go.dev/doc/install) -- Installed [Taskfile](https://taskfile.dev/installation) -- A Kubernetes cluster to connect to (some options below) - - Option A: Preexisting standard Kubernetes cluster - - Option B: Preexisting Kubernetes cluster that is available through [Kubernetes Control Plane (KCP)](https://docs.kcp.io/kcp/main/setup/quickstart/) - - Option C: Create your own locally running Kubernetes cluster using [kind](https://kind.sigs.k8s.io/) -- Clone the `kubernetes-graphql-gateway` repository and change to the root directory -```shell -git clone https://github.com/platform-mesh/kubernetes-graphql-gateway.git && cd kubernetes-graphql-gateway -``` - -## Operation Modes - -Don't skip this step. Please go to the [operation modes](../README.md#operation-modes) page and complete the required setup. - -## Running the Listener - -Make sure you have completed the steps from the [Prerequisites](#prerequisites) and [Operation Modes](#operation-modes) sections. - -```shell -task listener -``` -This will create a directory `./bin/definitions` and start watching the cluster APIs for changes. -In that directory a file will be created for each workspace in KCP or a standard Kubernetes cluster. -The file will contain the API definitions for the resources in that workspace. - -## Running the Gateway - -Make sure you have completed the steps from the [Prerequisites](#prerequisites) section. - -In the root directory of the `kubernetes-graphql-gateway` repository, open a new shell and run the GraphQL gateway as follows: -```shell -task gateway -``` - -The gateway will watch the `./bin/definitions` directory for changes and update the schema accordingly. -It will also spawn a GraphQL playground server that allows you to execute GraphQL queries via your browser. -Check the console output to get the localhost URL of the GraphQL playground. - -## First Steps and Basic Examples - -As mentioned above, the GraphQL Gateway allows you to do CRUD operations on any of the Kubernetes resources in the cluster. -You may check out the following copy & paste examples to get started: -- Examples on [CRUD operations on ConfigMaps](./configmap_queries.md) -- Examples on [CRUD operations on Pods](./pod_queries.md) -- Subscribe to events using [Subscriptions](./subscriptions.md) -- There are also [Custom Queries](./custom_queries.md) that go beyond standard CRUD operations - - -## Authorization with Remote Kubernetes Clusters - -If you run the GraphQL gateway with a shell environment that sets `LOCAL_DEVELOPMENT=false`, you need to add the `Authorization` header to any of your GraphQL queries you are executing. -When using the GraphQL playground, you can add the header in the `Headers` section of the playground user interface like so: -```shell -{ - "Authorization": "Bearer YOUR_TOKEN" -} -``` - -## Working with Dotted Keys (Labels, Annotations, NodeSelector, MatchLabels) - -Kubernetes extensively uses dotted keys (e.g., `app.kubernetes.io/name`) in labels, annotations, and other fields. Since GraphQL doesn't support dots in field names, the gateway provides a special `StringMapInput` scalar. - -**Key Points:** -- **Input**: Use variables with arrays of `{key, value}` objects -- **Output**: Returns direct maps like `{"app.kubernetes.io/name": "my-app"}` -- **Supported fields**: `metadata.labels`, `metadata.annotations`, `spec.nodeSelector`, `spec.selector.matchLabels`, and their nested equivalents in templates - -**Quick Example:** -```graphql -mutation createPodWithLabels($labels: StringMapInput) { - core { - createPod( - namespace: "default" - object: { - metadata: { - name: "my-pod" - labels: $labels - } - spec: { - containers: [...] - } - } - ) { - metadata { - labels # Returns: {"app.kubernetes.io/name": "my-app"} - } - } - } -} -``` - -**Variables:** -```json -{ - "labels": [ - {"key": "app.kubernetes.io/name", "value": "my-app"}, - {"key": "environment", "value": "production"} - ] -} -``` diff --git a/go.mod b/go.mod index c6eb990..b735b04 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( k8s.io/apimachinery v0.33.3 k8s.io/client-go v0.33.3 k8s.io/kube-openapi v0.0.0-20250701173324-9bd5c66d9911 + k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d sigs.k8s.io/controller-runtime v0.22.4 ) @@ -125,7 +126,6 @@ require ( k8s.io/apiserver v0.33.3 // indirect k8s.io/component-base v0.33.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect diff --git a/hack/create-clusteraccess.md b/hack/create-clusteraccess.md deleted file mode 100644 index 5056457..0000000 --- a/hack/create-clusteraccess.md +++ /dev/null @@ -1,14 +0,0 @@ -# Create ClusterAccess script - -This script is used to create a ClusterAccess resource, which is needed for kubernetes-graphql-gateway to work with Standard K8S cluster. - -More details about it you can find at [this readme](../docs/clusteraccess.md) - -## Usage - -```shell -./hack/create-clusteraccess.sh --target-kubeconfig $TARGET_CLUSTER_KUBECONFIG --management-kubeconfig $MANAGEMENT_CLUSTER_KUBECONFIG -``` -Where -- TARGET_CLUSTER_KUBECONFIG - path to the kubeconfig of the cluster we want the gateway to generate graphql schema -- MANAGEMENT_CLUSTER_KUBECONFIG - path to the kubeconfig of the cluster where ClusterAccess object will be created. It can be the same cluster as TARGET_CLUSTER_KUBECONFIG. \ No newline at end of file diff --git a/hack/create-clusteraccess.sh b/hack/create-clusteraccess.sh deleted file mode 100755 index a370f2b..0000000 --- a/hack/create-clusteraccess.sh +++ /dev/null @@ -1,324 +0,0 @@ -#!/bin/bash - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Default values -TARGET_KUBECONFIG="" -MANAGEMENT_KUBECONFIG="${KUBECONFIG:-$HOME/.kube/config}" -NAMESPACE="default" - -usage() { - echo "Usage: $0 --target-kubeconfig [options]" - echo "" - echo "Required:" - echo " --target-kubeconfig Path to target cluster kubeconfig" - echo "" - echo "Optional:" - echo " --management-kubeconfig Path to management cluster kubeconfig (default: \$KUBECONFIG or ~/.kube/config)" - echo " --namespace Namespace for secrets (default: default)" - echo " --help Show this help message" - echo "" - echo "Note: Cluster name will be extracted automatically from the target kubeconfig" - echo "" - echo "Authentication mode:" - echo " Uses target kubeconfig directly for full cluster admin access" - echo "" - echo "Example:" - echo " $0 --target-kubeconfig ~/.kube/target-config" -} - -log_info() { - echo -e "${GREEN}[INFO]${NC} $1" -} - -log_warn() { - echo -e "${YELLOW}[WARN]${NC} $1" -} - -log_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -# Parse command line arguments -while [[ $# -gt 0 ]]; do - case $1 in - --target-kubeconfig) - TARGET_KUBECONFIG="$2" - shift 2 - ;; - --management-kubeconfig) - MANAGEMENT_KUBECONFIG="$2" - shift 2 - ;; - --namespace) - NAMESPACE="$2" - shift 2 - ;; - --help) - usage - exit 0 - ;; - *) - log_error "Unknown option: $1" - usage - exit 1 - ;; - esac -done - -# Validate required arguments -if [[ -z "$TARGET_KUBECONFIG" ]]; then - log_error "Target kubeconfig path is required" - usage - exit 1 -fi - -# Validate files exist -if [[ ! -f "$TARGET_KUBECONFIG" ]]; then - log_error "Target kubeconfig file not found: $TARGET_KUBECONFIG" - exit 1 -fi - -if [[ ! -f "$MANAGEMENT_KUBECONFIG" ]]; then - log_error "Management kubeconfig file not found: $MANAGEMENT_KUBECONFIG" - exit 1 -fi - -# Extract cluster name from target kubeconfig -log_info "Extracting cluster name from target kubeconfig..." -CLUSTER_NAME=$(KUBECONFIG="$TARGET_KUBECONFIG" kubectl config view --raw -o jsonpath='{.clusters[0].name}') -if [[ -z "$CLUSTER_NAME" ]]; then - log_error "Failed to extract cluster name from kubeconfig" - exit 1 -fi -log_info "Cluster name: $CLUSTER_NAME" - -ensure_crd_installed() { - log_info "Ensuring ClusterAccess CRD is installed in management cluster..." - - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - CRD_PATH="$SCRIPT_DIR/../config/crd/gateway.platform-mesh.io_clusteraccesses.yaml" - - if [[ ! -f "$CRD_PATH" ]]; then - log_error "CRD file not found at: $CRD_PATH" - log_error "Please ensure the CRD file exists in the expected location" - exit 1 - fi - - if ! KUBECONFIG="$MANAGEMENT_KUBECONFIG" kubectl apply -f "$CRD_PATH"; then - log_error "Failed to apply ClusterAccess CRD" - exit 1 - fi - - log_info "Waiting for ClusterAccess CRD to become established..." - if ! KUBECONFIG="$MANAGEMENT_KUBECONFIG" kubectl wait --for=condition=Established crd/clusteraccesses.gateway.platform-mesh.io --timeout=60s; then - log_error "ClusterAccess CRD failed to reach Established condition within 60 seconds" - exit 1 - fi - - log_info "ClusterAccess CRD is established and ready" -} - -cleanup_existing_resources() { - log_info "Checking for existing ClusterAccess resource '$CLUSTER_NAME'..." - - SA_NAME="kubernetes-graphql-gateway-admin" - SA_NAMESPACE="default" - - # Check if ClusterAccess exists in management cluster - if KUBECONFIG="$MANAGEMENT_KUBECONFIG" kubectl get clusteraccess "$CLUSTER_NAME" &>/dev/null; then - log_warn "ClusterAccess '$CLUSTER_NAME' already exists. Cleaning up existing resources..." - - # Delete ClusterAccess resource - log_info "Deleting existing ClusterAccess resource..." - KUBECONFIG="$MANAGEMENT_KUBECONFIG" kubectl delete clusteraccess "$CLUSTER_NAME" --ignore-not-found=true - - # Delete related secrets in management cluster - log_info "Deleting existing secrets in management cluster..." - KUBECONFIG="$MANAGEMENT_KUBECONFIG" kubectl delete secret "${CLUSTER_NAME}-token" --namespace="$NAMESPACE" --ignore-not-found=true - KUBECONFIG="$MANAGEMENT_KUBECONFIG" kubectl delete secret "${CLUSTER_NAME}-ca" --namespace="$NAMESPACE" --ignore-not-found=true - KUBECONFIG="$MANAGEMENT_KUBECONFIG" kubectl delete secret "${CLUSTER_NAME}-admin-kubeconfig" --namespace="$NAMESPACE" --ignore-not-found=true - - log_info "Cleanup completed. Creating fresh resources..." - else - log_info "No existing ClusterAccess found. Creating new resources..." - fi - - # Clean up ServiceAccount and related resources in target cluster - if KUBECONFIG="$TARGET_KUBECONFIG" kubectl get serviceaccount "$SA_NAME" -n "$SA_NAMESPACE" &>/dev/null; then - log_info "Cleaning up existing ServiceAccount and related resources in target cluster..." - KUBECONFIG="$TARGET_KUBECONFIG" kubectl delete secret "${SA_NAME}-token" -n "$SA_NAMESPACE" --ignore-not-found=true - KUBECONFIG="$TARGET_KUBECONFIG" kubectl delete clusterrolebinding "${SA_NAME}-cluster-admin" --ignore-not-found=true - KUBECONFIG="$TARGET_KUBECONFIG" kubectl delete serviceaccount "$SA_NAME" -n "$SA_NAMESPACE" --ignore-not-found=true - fi -} - -log_info "Creating ClusterAccess resource '$CLUSTER_NAME'" -log_info "Target kubeconfig: $TARGET_KUBECONFIG" -log_info "Management kubeconfig: $MANAGEMENT_KUBECONFIG" -log_info "Authentication mode: Admin kubeconfig (full cluster access)" - -# Clean up existing resources if they exist -cleanup_existing_resources - -# Extract server URL from target kubeconfig -log_info "Extracting server URL from target kubeconfig..." -SERVER_URL=$(KUBECONFIG="$TARGET_KUBECONFIG" kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}') -if [[ -z "$SERVER_URL" ]]; then - log_error "Failed to extract server URL from kubeconfig" - exit 1 -fi -log_info "Server URL: $SERVER_URL" - -# Extract CA certificate from target kubeconfig -log_info "Extracting CA certificate from target kubeconfig..." -CA_DATA=$(KUBECONFIG="$TARGET_KUBECONFIG" kubectl config view --raw --minify -o jsonpath='{.clusters[0].cluster.certificate-authority-data}') -if [[ -z "$CA_DATA" ]]; then - log_error "Failed to extract CA certificate from kubeconfig" - exit 1 -fi - -# Decode CA certificate to verify it's valid -CA_CERT=$(echo "$CA_DATA" | base64 -d) -if [[ ! "$CA_CERT" =~ "BEGIN CERTIFICATE" ]]; then - log_error "Invalid CA certificate format" - exit 1 -fi -log_info "CA certificate extracted successfully" - -# Test target cluster connectivity -log_info "Testing target cluster connectivity..." -if ! KUBECONFIG="$TARGET_KUBECONFIG" kubectl cluster-info &>/dev/null; then - log_error "Cannot connect to target cluster" - exit 1 -fi -log_info "Target cluster is accessible" - -# Admin access mode: use kubeconfig directly -log_info "Using admin kubeconfig mode" - -# Test management cluster connectivity -log_info "Testing management cluster connectivity..." -if ! KUBECONFIG="$MANAGEMENT_KUBECONFIG" kubectl cluster-info &>/dev/null; then - log_error "Cannot connect to management cluster" - exit 1 -fi -log_info "Management cluster is accessible" - -# Create ServiceAccount with admin access in target cluster -log_info "Creating ServiceAccount with admin access in target cluster..." -SA_NAME="kubernetes-graphql-gateway-admin" -SA_NAMESPACE="default" - -cat <