Skip to content

Commit

Permalink
Get rid of environment variable reliance in pkg exposed config update…
Browse files Browse the repository at this point in the history
…r logic
  • Loading branch information
uvegla committed Feb 6, 2025
1 parent 7155175 commit 533fedc
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 87 deletions.
34 changes: 30 additions & 4 deletions cmd/kustomizepatch/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
kyaml "sigs.k8s.io/kustomize/kyaml/yaml"
"sigs.k8s.io/yaml"

cfg "github.com/giantswarm/konfigure/pkg/config"
"github.com/giantswarm/konfigure/pkg/configupdater"
"github.com/giantswarm/konfigure/pkg/meta"
"github.com/giantswarm/konfigure/pkg/service"
"github.com/giantswarm/konfigure/pkg/sopsenv/key"
Expand All @@ -38,16 +38,30 @@ const (

// dirEnvVar is a directory containing giantswarm/config. If set, requests
// to source-controller will not be made and both sourceServiceEnvVar and
// gitRepositoryEnvVar will be ignored. Used only on local machine for
// debugging.
dirEnvVar = "KONFIGURE_DIR"

// installationEnvVar tells konfigure which installation it's running in,
// e.g. "ginger"
installationEnvVar = "KONFIGURE_INSTALLATION"

// gitRepositoryEnvVar is namespace/name of GitRepository pointing to
// giantswarm/config, e.g. "flux-system/gs-config"
gitRepositoryEnvVar = "KONFIGURE_GITREPO"

// kubernetesServiceEnvVar is K8S host of the Kubernetes API service.
kubernetesServiceHostEnvVar = "KUBERNETES_SERVICE_HOST"

// kubernetesServicePortEnvVar is K8S port of the Kubernetes API service.
kubernetesServicePortEnvVar = "KUBERNETES_SERVICE_PORT"

// sourceServiceEnvVar is K8s address of source-controller's service, e.g.
// "source-controller.flux-system.svc"
sourceServiceEnvVar = "KONFIGURE_SOURCE_SERVICE"

// sopsKeysDirEnvVar tells Konfigure how to configure environment to make
// it possible for SOPS to find the keys
sopsKeysDirEnvVar = "KONFIGURE_SOPS_KEYS_DIR"

// sopsKeysSourceEnvVar tells Konfigure to either get keys from Kubernetes
// Secrets or rely on local storage when setting up environment for SOPS
sopsKeysSourceEnvVar = "KONFIGURE_SOPS_KEYS_SOURCE"
Expand Down Expand Up @@ -118,7 +132,19 @@ func (r *runner) run(items []*kyaml.RNode) ([]*kyaml.RNode, error) {
dir = os.Getenv(dirEnvVar)
// Else, we download the packaged config from source-controller.
if dir == "" {
if err := cfg.UpdateConfig(cacheDir); err != nil {
fluxUpdaterConfig := configupdater.Config{
CacheDir: cacheDir,
ApiServerHost: os.Getenv(kubernetesServiceHostEnvVar),
ApiServerPort: os.Getenv(kubernetesServicePortEnvVar),
SourceControllerService: os.Getenv(sourceServiceEnvVar),
GitRepository: os.Getenv(gitRepositoryEnvVar),
}
fluxUpdater, err := configupdater.New(fluxUpdaterConfig)
if err != nil {
return nil, microerror.Mask(err)
}

if err := fluxUpdater.UpdateConfig(); err != nil {
return nil, microerror.Mask(err)
}
dir = path.Join(cacheDir, "latest")
Expand Down
2 changes: 1 addition & 1 deletion pkg/config/error.go → pkg/configupdater/error.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package config
package configupdater

import "github.com/giantswarm/microerror"

Expand Down
145 changes: 87 additions & 58 deletions pkg/config/config.go → pkg/configupdater/flux.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package config
package configupdater

import (
"bytes"
Expand All @@ -20,63 +20,104 @@ import (
const (
cacheLastArchive = "lastarchive"
cacheLastArchiveTimestamp = "lastarchivetimestamp"
// gitRepositoryEnvVar is namespace/name of GitRepository pointing to
// giantswarm/config, e.g. "flux-system/gs-config"
gitRepositoryEnvVar = "KONFIGURE_GITREPO"
// kubernetesServiceEnvVar is K8S host of the Kubernetes API service.
kubernetesServiceHostEnvVar = "KUBERNETES_SERVICE_HOST"
// kubernetesServicePortEnvVar is K8S port of the Kubernetes API service.
kubernetesServicePortEnvVar = "KUBERNETES_SERVICE_PORT"
// kubernetesToken holds the location of the Kubernetes Service Account
// token mount within a Pod.
kubernetesTokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token" // #nosec G101

// v1SourceAPIGroup holds Flux Source group and v1 version
v1SourceAPIGroup = "source.toolkit.fluxcd.io/v1"
// v1beta2SourceAPIGroup holds Flux Source group and v1beta2 version
v1beta2SourceAPIGroup = "source.toolkit.fluxcd.io/v1beta2"
// sourceServiceEnvVar is K8s address of source-controller's service, e.g.
// "source-controller.flux-system.svc"
sourceServiceEnvVar = "KONFIGURE_SOURCE_SERVICE"

// defaultKubernetesTokenFile holds the location of the Kubernetes Service Account
// token mount within a Pod.
defaultKubernetesTokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token" // #nosec G101
)

// UpdateConfig makes sure that the giantswarm/config version we keep stashed
// in <cacheDir>/latest is still *the* latest version out there. In order to do that,
// it sends a HEAD request for the last known artifact to the Source Controller,
// in order to check it is still available. If so, it then skips further processing.
// Otherwise it contacts the GitRepository resource for the new artifact's URL.
// The URL is then used to download a new version of the archive and untar it.
// The archive name is being saved for later comparison.
func UpdateConfig(cacheDir string) error {
return UpdateConfigWithParams(cacheDir, kubernetesTokenFile)
type Config struct {
CacheDir string

ApiServerHost string
ApiServerPort string
KubernetesTokenFile string

SourceControllerService string

GitRepository string
}

type FluxUpdater struct {
CacheDir string

ApiServerHost string
ApiServerPort string
KubernetesTokenFile string

SourceControllerService string

GitRepository string
}

func UpdateConfigWithParams(cache, token string) error {
// Get source-controller's service URL and GitRepository data from
// environment variables. We use this data to construct an URL to
// source-controller's artifact.
svc := os.Getenv(sourceServiceEnvVar)
if svc == "" {
return microerror.Maskf(executionFailedError, "%q environment variable not set", sourceServiceEnvVar)
func New(config Config) (*FluxUpdater, error) {
if config.CacheDir == "" {
return nil, microerror.Maskf(invalidConfigError, "cacheDir must not be empty")
}

repo := os.Getenv(gitRepositoryEnvVar)
if repo == "" {
return microerror.Maskf(executionFailedError, "%q environment variable not set", gitRepositoryEnvVar)
if config.ApiServerHost == "" {
return nil, microerror.Maskf(invalidConfigError, "apiServerHost must not be empty")
}

if config.ApiServerPort == "" {
return nil, microerror.Maskf(invalidConfigError, "apiServerPort must not be empty")
}

var kubernetesTokenFile string
if config.KubernetesTokenFile == "" {
kubernetesTokenFile = defaultKubernetesTokenFile
} else {
kubernetesTokenFile = config.KubernetesTokenFile
}

if config.SourceControllerService == "" {
return nil, microerror.Maskf(invalidConfigError, "sourceControllerService must not be empty")
}

if config.GitRepository == "" {
return nil, microerror.Maskf(invalidConfigError, "gitRepository must not be empty")
}

if kubernetesTokenFile == "" {
kubernetesTokenFile = defaultKubernetesTokenFile
}

return &FluxUpdater{
CacheDir: config.CacheDir,
ApiServerHost: config.ApiServerHost,
ApiServerPort: config.ApiServerPort,
KubernetesTokenFile: kubernetesTokenFile,
SourceControllerService: config.SourceControllerService,
GitRepository: config.GitRepository,
}, nil
}

// UpdateConfig makes sure that the assembled CCR version we keep stashed
// in <cacheDir>/latest is still *the* latest version out there. In order to do that,
// it sends a HEAD request for the last known artifact to the Source Controller,
// in order to check it is still available. If so, it then skips further processing.
// Otherwise, it contacts the GitRepository resource for the new artifact's URL.
// The URL is then used to download a new version of the archive and untar it.
// The archive name is being saved for later comparison.
func (u *FluxUpdater) UpdateConfig() error {
// We first get the 'lastarchive' file, because it contains the name of the artifact we have
// been using up until now. If the file is gone, it means we haven't populated the cache yet,
// hence we must do it now. If the file is present, but archive of the given name is no longer
// advertised by the Source Controller, we must look for a new one and re-populate the cache. If the
// file is present, and is still advertised by the Source Controller, all is good and we may return.
cachedArtifact, err := os.ReadFile(path.Join(cache, cacheLastArchive))
cachedArtifact, err := os.ReadFile(path.Join(u.CacheDir, cacheLastArchive))
if err != nil && os.IsNotExist(err) {
cachedArtifact = []byte("placeholder.tar.gz")
} else if err != nil {
return microerror.Mask(err)
}

cachedArtifactTimestampByte, err := os.ReadFile(path.Join(cache, cacheLastArchiveTimestamp))
cachedArtifactTimestampByte, err := os.ReadFile(path.Join(u.CacheDir, cacheLastArchiveTimestamp))
if err != nil && os.IsNotExist(err) {
cachedArtifactTimestampByte = []byte(time.Time{}.Format(http.TimeFormat))
} else if err != nil {
Expand All @@ -88,7 +129,7 @@ func UpdateConfigWithParams(cache, token string) error {
return microerror.Mask(err)
}

url := fmt.Sprintf("http://%s/gitrepository/%s/%s", svc, repo, string(cachedArtifact))
url := fmt.Sprintf("http://%s/gitrepository/%s/%s", u.SourceControllerService, u.GitRepository, string(cachedArtifact))
// Make a HEAD request to the Source Controller. This allows us to check if the artifact
// we have cached is still offered.
client := &http.Client{Timeout: 60 * time.Second}
Expand Down Expand Up @@ -130,40 +171,28 @@ func UpdateConfigWithParams(cache, token string) error {
// When latest known revision is still available, there is no need to query the API Server
// for the GitRepository, it saves us one call.
if url == "" {
// The artifact we were asking for is gone, we must find the newly advertised one,
// hence we query the Kubernetes API Server for the GitRepository CR resource.
k8sApiHost := os.Getenv(kubernetesServiceHostEnvVar)
if svc == "" {
return microerror.Maskf(executionFailedError, "%q environment variable not set", kubernetesServiceHostEnvVar)
}

k8sApiPort := os.Getenv(kubernetesServicePortEnvVar)
if svc == "" {
return microerror.Maskf(executionFailedError, "%q environment variable not set", kubernetesServicePortEnvVar)
}

repoCoordinates := strings.Split(repo, "/")
repoCoordinates := strings.Split(u.GitRepository, "/")

k8sApiPath := []string{
fmt.Sprintf(
"https://%s:%s/apis/%s/namespaces/%s/gitrepositories/%s",
k8sApiHost,
k8sApiPort,
u.ApiServerHost,
u.ApiServerPort,
v1SourceAPIGroup,
repoCoordinates[0],
repoCoordinates[1],
),
fmt.Sprintf(
"https://%s:%s/apis/%s/namespaces/%s/gitrepositories/%s",
k8sApiHost,
k8sApiPort,
u.ApiServerHost,
u.ApiServerPort,
v1beta2SourceAPIGroup,
repoCoordinates[0],
repoCoordinates[1],
),
}

k8sToken, err := os.ReadFile(token)
k8sToken, err := os.ReadFile(u.KubernetesTokenFile)
if err != nil {
return microerror.Mask(err)
}
Expand Down Expand Up @@ -207,7 +236,7 @@ func UpdateConfigWithParams(cache, token string) error {
if response.StatusCode != http.StatusOK {
return microerror.Maskf(
executionFailedError,
"error getting '%s' GitRepository CR", repo,
"error getting '%s' GitRepository CR", u.GitRepository,
)
}

Expand Down Expand Up @@ -269,7 +298,7 @@ func UpdateConfigWithParams(cache, token string) error {
}

// Clear the old artifact's directory and untar a fresh one.
dir := path.Join(cache, "latest")
dir := path.Join(u.CacheDir, "latest")
if err := os.RemoveAll(dir); err != nil {
return microerror.Mask(err)
}
Expand All @@ -281,12 +310,12 @@ func UpdateConfigWithParams(cache, token string) error {
}

// Update the last archive name and timestamp
err = os.WriteFile(path.Join(cache, cacheLastArchive), []byte(filepath.Base(url)), 0755) // nolint:gosec
err = os.WriteFile(path.Join(u.CacheDir, cacheLastArchive), []byte(filepath.Base(url)), 0755) // nolint:gosec
if err != nil {
return microerror.Mask(err)
}

err = os.WriteFile(path.Join(cache, cacheLastArchiveTimestamp), []byte(response.Header.Get("Last-Modified")), 0755) // nolint:gosec
err = os.WriteFile(path.Join(u.CacheDir, cacheLastArchiveTimestamp), []byte(response.Header.Get("Last-Modified")), 0755) // nolint:gosec
if err != nil {
return microerror.Mask(err)
}
Expand Down
31 changes: 22 additions & 9 deletions pkg/config/config_test.go → pkg/configupdater/flux_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package config
package configupdater

import (
"fmt"
Expand Down Expand Up @@ -62,12 +62,6 @@ func TestRunner_updateConfig(t *testing.T) {
panic(err)
}

// Export appropriate environment variables to configure the runner
t.Setenv("KONFIGURE_SOURCE_SERVICE", fmt.Sprintf("%s:%s", srcCtrlUrl.Hostname(), srcCtrlUrl.Port()))
t.Setenv("KONFIGURE_GITREPO", "flux-giantswarm/giantswarm-config")
t.Setenv("KUBERNETES_SERVICE_HOST", k8sUrl.Hostname())
t.Setenv("KUBERNETES_SERVICE_PORT", k8sUrl.Port())

testCases := []struct {
name string
deprecatedPresent bool
Expand Down Expand Up @@ -127,7 +121,12 @@ func TestRunner_updateConfig(t *testing.T) {
if err != nil {
t.Fatalf("want nil, got error: %s", err.Error())
}
defer os.RemoveAll(tmpCacheDir)
defer func(path string) {
err := os.RemoveAll(path)
if err != nil {
t.Fatalf("want nil, got error: %s", err.Error())
}
}(tmpCacheDir)

err = prePopulateCache(
tmpCacheDir,
Expand All @@ -139,8 +138,22 @@ func TestRunner_updateConfig(t *testing.T) {
t.Fatalf("want nil, got error: %s", err.Error())
}

fluxUpdaterConfig := Config{
CacheDir: tmpCacheDir,
ApiServerHost: k8sUrl.Hostname(),
ApiServerPort: k8sUrl.Port(),
KubernetesTokenFile: "testdata/token",
SourceControllerService: fmt.Sprintf("%s:%s", srcCtrlUrl.Hostname(), srcCtrlUrl.Port()),
GitRepository: "flux-giantswarm/giantswarm-config",
}

fluxUpdater, err := New(fluxUpdaterConfig)
if err != nil {
t.Fatalf("want nil, got error: %s", err.Error())
}

// run updateConfigWithParams
err = UpdateConfigWithParams(tmpCacheDir, "testdata/token")
err = fluxUpdater.UpdateConfig()
if err != nil {
t.Fatalf("want nil, got error: %s", err.Error())
}
Expand Down
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit 533fedc

Please sign in to comment.