pkg/cli/preview/preview.go (90 lines of code) (raw):
// Copyright 2025 Google LLC
//
// 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 preview
import (
"context"
"fmt"
"net/http"
"golang.org/x/oauth2"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/manager"
_ "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/register"
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/kccmanager"
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/kccmanager/nocache"
"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/structuredreporting"
transport_tpg "github.com/hashicorp/terraform-provider-google-beta/google-beta/transport"
)
// PreviewInstance runs KCC but intercepts GCP and Kubernetes API calls.
// We allow read-only operations to pass through, but block and log write operations.
// It is useful for testing the behavior of KCC without actually making any changes to GCP or Kubernetes.
type PreviewInstance struct {
mgr manager.Manager
hookGCP *interceptingGCPClient
hookKube *interceptingKubeClient
recorder *Recorder
}
// PreviewInstanceOptions are the options for creating a PreviewInstance.
type PreviewInstanceOptions struct {
// UpstreamRESTConfig is the rest configuration to use when talking to upstream (real) kube-apiserver
// (Upstream kube-apiserver may be mocked in tests)
UpstreamRESTConfig *rest.Config
// UpstreamGCPAuthorization is the authorization to use when talking to upstream (real) GCP
// (Upstream GCP may be mocked in tests)
UpstreamGCPAuthorization oauth2.TokenSource
// UpstreamGCPHTTPClient is the http client to use when talking to upstream (real) GCP
// (Upstream GCP may be mocked in tests)
UpstreamGCPHTTPClient *http.Client
}
// NewPreviewInstance creates a new PreviewInstance.
func NewPreviewInstance(recorder *Recorder, options PreviewInstanceOptions) (*PreviewInstance, error) {
upstreamRESTConfig := options.UpstreamRESTConfig
authorization := options.UpstreamGCPAuthorization
upstreamGCPHTTPClient := options.UpstreamGCPHTTPClient
if upstreamGCPHTTPClient == nil {
upstreamGCPHTTPClient = http.DefaultClient
}
hookKube, err := newInterceptingKubeClient(recorder, upstreamRESTConfig)
if err != nil {
return nil, err
}
hookGCP := newInterceptingGCPClient(upstreamGCPHTTPClient, authorization)
i := &PreviewInstance{}
i.hookGCP = hookGCP
i.hookKube = hookKube
i.recorder = recorder
return i, nil
}
type httpRoundTripperKeyType int
// httpRoundTripperKey is the key value for http.RoundTripper in a context.Context
var httpRoundTripperKey httpRoundTripperKeyType
// Start starts the PreviewInstance.
func (i *PreviewInstance) Start(ctx context.Context) error {
grpcUnaryInterceptor := i.hookGCP.GRPCUnaryClientInterceptor()
gcpHTTPClient := i.hookGCP.HTTPClient()
// Store our http client in the context
ctx = context.WithValue(ctx, httpRoundTripperKey, i.hookGCP.HTTPRoundTripper())
// Also hook the oauth2 library
ctx = context.WithValue(ctx, oauth2.HTTPClient, gcpHTTPClient)
ctx = structuredreporting.ContextWithListener(ctx, i.recorder.NewStructuredReportingListener())
// Intercept (and log) TF requests
transport_tpg.GRPCUnaryClientInterceptor = grpcUnaryInterceptor
transport_tpg.DefaultHTTPClientTransformer = func(ctx context.Context, inner *http.Client) *http.Client {
ret := inner
if t := ctx.Value(httpRoundTripperKey); t != nil {
ret = &http.Client{Transport: t.(http.RoundTripper)}
}
return ret
}
// Intercept (and log) TF oauth requests
transport_tpg.OAuth2HTTPClientTransformer = func(ctx context.Context, inner *http.Client) *http.Client {
ret := inner
if t := ctx.Value(httpRoundTripperKey); t != nil {
ret = &http.Client{Transport: t.(http.RoundTripper)}
}
return ret
}
kccConfig := kccmanager.Config{}
// Prevent manager from binding to a port to serve prometheus metrics
// since creating multiple managers for tests will fail if more than
// one manager tries to bind to the same port.
kccConfig.ManagerOptions.MetricsBindAddress = "0"
// Prevent manager from binding to a port to serve health probes since
// creating multiple managers for tests will fail if more than one
// manager tries to bind to the same port.
kccConfig.ManagerOptions.HealthProbeBindAddress = "0"
// Hook kube
kccConfig.ManagerOptions.NewCache = i.hookKube.NewCache
kccConfig.ManagerOptions.NewClient = i.hookKube.NewClient
kccConfig.ManagerOptions.BaseContext = func() context.Context {
return ctx
}
kccConfig.ManagerOptions.MapperProvider = i.hookKube.MapperProvider
// turn off caching (otherwise we get partial object metadata)
nocache.OnlyCacheCCAndCCC(&kccConfig.ManagerOptions)
// Use an empty restConfig as a failsafe against requests "leaking" to real kube-apiserver
restConfig := &rest.Config{}
// Hook GCP
kccConfig.GRPCUnaryClientInterceptor = grpcUnaryInterceptor
kccConfig.HTTPClient = gcpHTTPClient
kccConfig.GCPAccessToken = "dummytoken" // Use a fake token as a failsafe against requests "leaking" to real GCP
mgr, err := kccmanager.New(ctx, restConfig, kccConfig)
if err != nil {
return fmt.Errorf("creating controllers: %w", err)
}
i.mgr = mgr
// We don't currently set up webhooks, as they are normally mutuating and shouldn't be part of preview functionality.
// if len(webhooks) > 0 {
// server := mgr.GetWebhookServer()
// for _, cfg := range webhooks {
// handler := cfg.HandlerFunc(mgr)
// server.Register(cfg.Path, &webhook.Admission{Handler: handler})
// }
// }
if err := mgr.Start(ctx); err != nil {
return fmt.Errorf("starting controllers: %w", err)
}
return nil
}