Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ require (
k8s.io/apimachinery v0.33.3
k8s.io/cli-runtime v0.33.3
k8s.io/client-go v0.33.3
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
sigs.k8s.io/e2e-framework v0.6.0
sigs.k8s.io/yaml v1.5.0
)
Expand Down Expand Up @@ -145,6 +144,7 @@ require (
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
k8s.io/kubectl v0.33.3 // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
oras.land/oras-go/v2 v2.6.0 // indirect
sigs.k8s.io/controller-runtime v0.20.0 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
Expand Down
8 changes: 4 additions & 4 deletions internal/handlers/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ package handlers

import (
"log/slog"
"net/http"

"github.com/krateoplatformops/chart-inspector/internal/helmclient"
"k8s.io/client-go/dynamic"
)

type HandlerOptions struct {
Log *slog.Logger
Client *http.Client
HelmClientOptions *helmclient.RestConfClientOptions
Log *slog.Logger
// Client *http.Client
HelmClientOptions helmclient.RestConfClientOptions
Clientset helmclient.CachedClients
DynamicClient dynamic.Interface
KrateoNamespace string
}
51 changes: 31 additions & 20 deletions internal/handlers/resources/get/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/rest"
sigsyaml "sigs.k8s.io/yaml"
)

Expand Down Expand Up @@ -58,13 +59,13 @@ var _ http.Handler = (*handler)(nil)
// @Success 200 {object} []Resource
// @Router /resources [get]
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
h.Log.Error("panic in ServeHTTP",
slog.Any("panic", rec))
response.InternalError(w, fmt.Errorf("internal server error"))
}
}()
// defer func() {
// if rec := recover(); rec != nil {
// h.Log.Error("panic in ServeHTTP",
// slog.Any("panic", rec))
// response.InternalError(w, fmt.Errorf("internal server error"))
// }
// }()

compositionName := r.URL.Query().Get("compositionName")
compositionNamespace := r.URL.Query().Get("compositionNamespace")
Expand Down Expand Up @@ -125,18 +126,21 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}

tracer := &tracer.Tracer{}
// Getting the resources
h.HelmClientOptions.RestConfig.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
return tracer.WithRoundTripper(rt)
localOpts := h.HelmClientOptions
if localOpts.RestConfig != nil {
localOpts.RestConfig = rest.CopyConfig(localOpts.RestConfig)
localOpts.RestConfig.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
return tracer.WithRoundTripper(rt)
}
}

h.HelmClientOptions.Options = &helmclient.Options{
localOpts.Options = &helmclient.Options{
Namespace: composition.GetNamespace(),
}

helmcli, err := helmclient.NewClientFromRestConf(h.HelmClientOptions)
helmcli, err := helmclient.NewCachedClientFromRestConf(&localOpts, &h.Clientset)
if err != nil {
h.Log.Error("unable to create helm client",
log.Error("unable to create helm client",
slog.Any("err", err),
)
response.InternalError(w, err)
Expand All @@ -145,7 +149,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

bValues, err := ExtractValuesFromSpec(composition)
if err != nil {
h.Log.Error("unable to extract values from composition",
log.Error("unable to extract values from composition",
slog.Any("err", err),
)
response.InternalError(w, err)
Expand All @@ -157,7 +161,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Namespace(compositionDefinitionNamespace).
Get(context.Background(), compositionDefinitionName, v1.GetOptions{})
if err != nil {
h.Log.Error("unable to get composition definition",
log.Error("unable to get composition definition",
slog.Any("err", err),
)
response.InternalError(w, err)
Expand All @@ -166,7 +170,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var compositionDefinition coreprovv1.CompositionDefinition
err = runtime.DefaultUnstructuredConverter.FromUnstructured(compositionDefinitionU.Object, &compositionDefinition)
if err != nil {
h.Log.Error("unable to convert composition definition",
log.Error("unable to convert composition definition",
slog.Any("err", err),
)
response.InternalError(w, err)
Expand All @@ -183,6 +187,13 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
CompositionKind: composition.GetKind(),
GracefullyPaused: composition.GetAnnotations()[AnnotationKeyReconciliationGracefullyPaused] == "true",
})
if err != nil {
log.Error("unable to inject values",
slog.Any("err", err),
)
response.InternalError(w, err)
return
}

chartSpec := helmclient.ChartSpec{
InsecureSkipTLSverify: compositionDefinition.Spec.Chart.InsecureSkipVerifyTLS,
Expand All @@ -196,7 +207,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if compositionDefinition.Spec.Chart != nil && compositionDefinition.Spec.Chart.Credentials != nil {
passwd, err := k8scli.GetSecret(compositionDefinition.Spec.Chart.Credentials.PasswordRef)
if err != nil {
h.Log.Error("unable to get secret",
log.Error("unable to get secret",
slog.Any("err", err),
)
response.InternalError(w, err)
Expand All @@ -209,7 +220,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

_, err = helmcli.TemplateChartRaw(&chartSpec, nil)
if err != nil {
h.Log.Error("unable to template chart",
log.Error("unable to template chart",
slog.Any("err", err),
)

Expand All @@ -230,14 +241,14 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
enc := json.NewEncoder(w)
err = enc.Encode(resLi)
if err != nil {
h.Log.Error("unable to marshal resources",
log.Error("unable to marshal resources",
slog.Any("err", err),
)
response.InternalError(w, err)
return
}

h.Log.Info("Successfully handled request to get resources")
log.Info("Successfully handled request to get resources")
}

func ExtractValuesFromSpec(un *unstructured.Unstructured) ([]byte, error) {
Expand Down
23 changes: 16 additions & 7 deletions internal/handlers/resources/get/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"gotest.tools/v3/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/dynamic"
"k8s.io/utils/ptr"

"context"
"os"
Expand Down Expand Up @@ -174,14 +173,20 @@ func TestResourcesHandler(t *testing.T) {
values.Add("compositionNamespace", composition.GetNamespace())
req.URL.RawQuery = values.Encode()

clientset, err := helmclient.NewCachedClients(c.Client().RESTConfig())
if err != nil {
t.Fatal("Creating cached clientset.", err)
}

rec := httptest.NewRecorder()
h := GetResources(handlers.HandlerOptions{
Log: slog.Default(),
Client: http.DefaultClient,
DynamicClient: dynamic,
HelmClientOptions: ptr.To(helmclient.RestConfClientOptions{
HelmClientOptions: helmclient.RestConfClientOptions{
RestConfig: c.Client().RESTConfig(),
}),
},
Clientset: clientset,
KrateoNamespace: "test-system",
})

h.ServeHTTP(rec, req)
Expand Down Expand Up @@ -239,14 +244,18 @@ func TestResourcesHandlerErrorCases(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, tt.url, nil)
rec := httptest.NewRecorder()
clientset, err := helmclient.NewCachedClients(c.Client().RESTConfig())
if err != nil {
t.Fatal("Creating cached clientset.", err)
}

h := GetResources(handlers.HandlerOptions{
Log: slog.Default(),
Client: http.DefaultClient,
DynamicClient: dynamic,
HelmClientOptions: ptr.To(helmclient.RestConfClientOptions{
HelmClientOptions: helmclient.RestConfClientOptions{
RestConfig: c.Client().RESTConfig(),
}),
},
Clientset: clientset,
})

h.ServeHTTP(rec, req)
Expand Down
12 changes: 12 additions & 0 deletions internal/helmclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ func NewClientFromRestConf(options *RestConfClientOptions) (Client, error) {
return newClient(options.Options, clientGetter, settings)
}

// NewClientFromRestConf returns a new Helm client constructed with the provided REST config options.
func NewCachedClientFromRestConf(options *RestConfClientOptions, clientset *CachedClients) (Client, error) {
settings := cli.New()

clientGetter := NewCachedRESTClientGetter(options.Namespace, nil, options.RestConfig, clientset)

return newClient(options.Options, clientGetter, settings)
}

// newClient is used by both NewClientFromKubeConf and NewClientFromRestConf
// and returns a new Helm client via the provided options and REST config.
func newClient(options *Options, clientGetter genericclioptions.RESTClientGetter, settings *cli.EnvSettings) (Client, error) {
Expand Down Expand Up @@ -686,6 +695,8 @@ func (c *HelmClient) TemplateChartRaw(spec *ChartSpec, options *HelmTemplateOpti
client.ClientOnly = false
client.IncludeCRDs = true
client.DryRunOption = "server"
client.SkipSchemaValidation = true
client.DisableOpenAPIValidation = true

if options != nil {
client.KubeVersion = options.KubeVersion
Expand Down Expand Up @@ -1044,6 +1055,7 @@ func (c *HelmClient) GetChartV2(spec *ChartInfo) (*chart.Chart, string, error) {
if err != nil {
return nil, "", fmt.Errorf("failed to get chart %q: %w", spec.Url, err)
}
defer bChart.Close()

helmChart, err := loader.LoadArchive(bChart)
if err != nil {
Expand Down
79 changes: 79 additions & 0 deletions internal/helmclient/client_getter.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"k8s.io/client-go/rest"
"k8s.io/client-go/restmapper"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)

// NewRESTClientGetter returns a RESTClientGetter using the provided 'namespace', 'kubeConfig' and 'restConfig'.
Expand Down Expand Up @@ -73,3 +74,81 @@ func (c *RESTClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {

return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)
}

type CachedClients struct {
discoveryClient discovery.CachedDiscoveryInterface
_RESTMapper meta.RESTMapper
}

func NewCachedClients(cfg *rest.Config) (CachedClients, error) {
discovery, err := discovery.NewDiscoveryClientForConfig(cfg)
if err != nil {
return CachedClients{}, err
}
cachedDiscovery := memory.NewMemCacheClient(discovery)
mapper := restmapper.NewDeferredDiscoveryRESTMapper(cachedDiscovery)
expander := restmapper.NewShortcutExpander(mapper, cachedDiscovery, nil)

return CachedClients{
discoveryClient: cachedDiscovery,
_RESTMapper: expander,
}, nil
}

var _ clientcmd.ClientConfig = &namespaceClientConfig{}

type namespaceClientConfig struct {
namespace string
}

func (c namespaceClientConfig) RawConfig() (clientcmdapi.Config, error) {
return clientcmdapi.Config{}, nil
}

func (c namespaceClientConfig) ClientConfig() (*rest.Config, error) {
return nil, nil
}

func (c namespaceClientConfig) Namespace() (string, bool, error) {
return c.namespace, false, nil
}

func (c namespaceClientConfig) ConfigAccess() clientcmd.ConfigAccess {
return nil
}

// NewRESTClientGetter returns a RESTClientGetter using the provided 'namespace', 'kubeConfig' and 'restConfig'.
// source: https://github.com/helm/helm/issues/6910#issuecomment-601277026
func NewCachedRESTClientGetter(namespace string, kubeConfig []byte, restConfig *rest.Config, clients *CachedClients, opts ...RESTClientOption) *CachedRESTClientGetter {
return &CachedRESTClientGetter{
kubeConfig: kubeConfig,
restConfig: restConfig,
discoveryClient: clients.discoveryClient,
restMapper: clients._RESTMapper,
namespaceConfig: &namespaceClientConfig{namespace: namespace},
opts: opts,
}
}

// ToRESTConfig returns a REST config build from a given kubeconfig
func (c *CachedRESTClientGetter) ToRESTConfig() (*rest.Config, error) {
if c.restConfig != nil {
return c.restConfig, nil
}

return clientcmd.RESTConfigFromKubeConfig(c.kubeConfig)

}

// ToDiscoveryClient returns a CachedDiscoveryInterface that can be used as a discovery client.
func (c *CachedRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
return c.discoveryClient, nil
}

func (c *CachedRESTClientGetter) ToRESTMapper() (meta.RESTMapper, error) {
return c.restMapper, nil
}

func (c *CachedRESTClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
return c.namespaceConfig
}
Loading