diff --git a/cmd/agent/docker/docker_actions.go b/cmd/agent/docker/docker_actions.go index 58fb3c41b..32dd94cca 100644 --- a/cmd/agent/docker/docker_actions.go +++ b/cmd/agent/docker/docker_actions.go @@ -7,10 +7,14 @@ import ( "fmt" "os" "os/exec" + "strings" + dockerconfig "github.com/docker/cli/cli/config" "github.com/glasskube/distr/api" "github.com/glasskube/distr/internal/agentauth" + "github.com/glasskube/distr/internal/agentenv" "go.uber.org/zap" + "gopkg.in/yaml.v3" ) func ApplyComposeFile(ctx context.Context, deployment api.DockerAgentDeployment) (*AgentDeployment, string, error) { @@ -46,7 +50,7 @@ func ApplyComposeFile(ctx context.Context, deployment api.DockerAgentDeployment) cmd := exec.CommandContext(ctx, "docker", composeArgs...) cmd.Stdin = bytes.NewReader(deployment.ComposeFile) - cmd.Env = append(os.Environ(), agentauth.DockerConfigEnv(deployment.AgentDeployment)...) + cmd.Env = append(os.Environ(), DockerConfigEnv(deployment)...) var cmdOut []byte cmdOut, err = cmd.CombinedOutput() @@ -68,3 +72,32 @@ func UninstallDockerCompose(ctx context.Context, deployment AgentDeployment) err } return nil } + +func DockerConfigEnv(deployment api.DockerAgentDeployment) []string { + if len(deployment.RegistryAuth) > 0 || hasRegistryImages(deployment) { + return []string{ + dockerconfig.EnvOverrideConfigDir + "=" + agentauth.DockerConfigDir(deployment.AgentDeployment), + } + } else { + return nil + } +} + +// hasRegistryImages parses the compose file in order to check whether one of the services uses an image hosted on +// [agentenv.DistrRegistryHost]. +func hasRegistryImages(deployment api.DockerAgentDeployment) bool { + var compose struct { + Services map[string]struct { + Image string + } + } + if err := yaml.Unmarshal(deployment.ComposeFile, &compose); err != nil { + return false + } + for _, svc := range compose.Services { + if strings.HasPrefix(svc.Image, agentenv.DistrRegistryHost) { + return true + } + } + return false +} diff --git a/cmd/agent/docker/main.go b/cmd/agent/docker/main.go index 89c1b4893..1a3a2ff7a 100644 --- a/cmd/agent/docker/main.go +++ b/cmd/agent/docker/main.go @@ -2,13 +2,13 @@ package main import ( "context" - "os" "os/signal" "syscall" "time" "github.com/glasskube/distr/internal/agentauth" "github.com/glasskube/distr/internal/agentclient" + "github.com/glasskube/distr/internal/agentenv" "github.com/glasskube/distr/internal/types" "github.com/glasskube/distr/internal/util" "go.uber.org/multierr" @@ -16,31 +16,19 @@ import ( ) var ( - interval = 5 * time.Second - logger = util.Require(zap.NewDevelopment()) - client = util.Require(agentclient.NewFromEnv(logger)) - agentVersionID = os.Getenv("DISTR_AGENT_VERSION_ID") + logger = util.Require(zap.NewDevelopment()) + client = util.Require(agentclient.NewFromEnv(logger)) ) func init() { - if intervalStr, ok := os.LookupEnv("DISTR_INTERVAL"); ok { - interval = util.Require(time.ParseDuration(intervalStr)) - } - if agentVersionID == "" { - logger.Warn("DISTR_AGENT_VERSION_ID is not set. self updates will be disabled") + if agentenv.AgentVersionID == "" { + logger.Warn("AgentVersionID is not set. self updates will be disabled") } } func main() { - ctx, cancel := context.WithCancel(context.Background()) - go func() { - sigint := make(chan os.Signal, 1) - signal.Notify(sigint, syscall.SIGTERM, syscall.SIGINT) - <-sigint - logger.Info("received termination signal") - cancel() - }() - tick := time.Tick(interval) + ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) + tick := time.Tick(agentenv.Interval) loop: for ctx.Err() == nil { select { @@ -52,8 +40,8 @@ loop: if resource, err := client.DockerResource(ctx); err != nil { logger.Error("failed to get resource", zap.Error(err)) } else { - if agentVersionID != "" { - if agentVersionID != resource.Version.ID.String() { + if agentenv.AgentVersionID != "" { + if agentenv.AgentVersionID != resource.Version.ID.String() { logger.Info("agent version has changed. starting self-update") if err := RunAgentSelfUpdate(ctx); err != nil { logger.Error("self update failed", zap.Error(err)) @@ -95,10 +83,10 @@ loop: progressCtx, progressCancel := context.WithCancel(ctx) go func(ctx context.Context) { - tick := time.Tick(interval) + tick := time.Tick(agentenv.Interval) for { select { - case <-progressCtx.Done(): + case <-ctx.Done(): logger.Info("stop sending progress updates") return case <-tick: @@ -118,7 +106,7 @@ loop: var agentDeployment *AgentDeployment var status string - _, err = agentauth.EnsureAuth(ctx, resource.Deployment.AgentDeployment) + _, err = agentauth.EnsureAuth(ctx, client.RawToken(), resource.Deployment.AgentDeployment) if err != nil { logger.Error("docker auth error", zap.Error(err)) } else if agentDeployment, status, err = ApplyComposeFile(ctx, *resource.Deployment); err == nil { diff --git a/cmd/agent/kubernetes/agent_deployment.go b/cmd/agent/kubernetes/agent_deployment.go index c9aab0bf4..3a41123c9 100644 --- a/cmd/agent/kubernetes/agent_deployment.go +++ b/cmd/agent/kubernetes/agent_deployment.go @@ -23,6 +23,10 @@ func (d *AgentDeployment) SecretName() string { return fmt.Sprintf("sh.distr.agent.v1.%v", d.ReleaseName) } +func PullSecretName(releaseName string) string { + return fmt.Sprintf("sh.distr.agent.v1.%v.pull", releaseName) +} + func GetExistingDeployments(ctx context.Context, namespace string) ([]AgentDeployment, error) { if secrets, err := k8sClient.CoreV1().Secrets(namespace). List(ctx, metav1.ListOptions{LabelSelector: LabelDeplyoment}); err != nil { diff --git a/cmd/agent/kubernetes/helm_actions.go b/cmd/agent/kubernetes/helm_actions.go index b70f9a7b6..f5d8aeb63 100644 --- a/cmd/agent/kubernetes/helm_actions.go +++ b/cmd/agent/kubernetes/helm_actions.go @@ -7,6 +7,7 @@ import ( "github.com/glasskube/distr/api" "github.com/glasskube/distr/internal/agentauth" + "github.com/glasskube/distr/internal/agentenv" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" @@ -24,17 +25,23 @@ var ( func GetHelmActionConfig( ctx context.Context, namespace string, - deployment *api.AgentDeployment, + deployment *api.KubernetesAgentDeployment, ) (*action.Configuration, error) { if cfg, ok := helmActionConfigCache[namespace]; ok { return cfg, nil } var cfg action.Configuration + var clientOpts []registry.ClientOption + if agentenv.DistrRegistryPlainHTTP { + clientOpts = append(clientOpts, registry.ClientOptPlainHTTP()) + } if deployment != nil { - if authorizer, err := agentauth.EnsureAuth(ctx, *deployment); err != nil { + if authorizer, err := + agentauth.EnsureAuth(ctx, agentClient.RawToken(), deployment.AgentDeployment); err != nil { return nil, err - } else if rc, err := registry.NewClient(registry.ClientOptAuthorizer(authorizer)); err != nil { + } else if rc, err := + registry.NewClient(append(clientOpts, registry.ClientOptAuthorizer(authorizer))...); err != nil { return nil, err } else { cfg.RegistryClient = rc @@ -44,7 +51,7 @@ func GetHelmActionConfig( k8sConfigFlags, namespace, "secret", - func(format string, v ...interface{}) { logger.Sugar().Debugf(format, v...) }, + func(format string, v ...any) { logger.Sugar().Debugf(format, v...) }, ); err != nil { return nil, err } else { @@ -57,7 +64,7 @@ func GetLatestHelmRelease( namespace string, deployment api.KubernetesAgentDeployment, ) (*release.Release, error) { - cfg, err := GetHelmActionConfig(ctx, namespace, &deployment.AgentDeployment) + cfg, err := GetHelmActionConfig(ctx, namespace, nil) if err != nil { return nil, err } @@ -85,6 +92,7 @@ func RunHelmPreflight( } else if chart, err := loader.Load(chartPath); err != nil { return nil, fmt.Errorf("chart loading failed: %w", err) } else { + addImagePullSecretToValues(deployment.ReleaseName, deployment.Values) return chart, nil } } @@ -94,7 +102,7 @@ func RunHelmInstall( namespace string, deployment api.KubernetesAgentDeployment, ) (*AgentDeployment, error) { - config, err := GetHelmActionConfig(ctx, namespace, &deployment.AgentDeployment) + config, err := GetHelmActionConfig(ctx, namespace, &deployment) if err != nil { return nil, err } @@ -106,6 +114,7 @@ func RunHelmInstall( installAction.Wait = true installAction.Atomic = true installAction.Namespace = namespace + installAction.PlainHTTP = agentenv.DistrRegistryPlainHTTP if chart, err := RunHelmPreflight(&installAction.ChartPathOptions, deployment); err != nil { return nil, fmt.Errorf("helm preflight failed: %w", err) } else if release, err := installAction.RunWithContext(ctx, chart, deployment.Values); err != nil { @@ -114,6 +123,7 @@ func RunHelmInstall( return &AgentDeployment{ ReleaseName: release.Name, HelmRevision: release.Version, + ID: deployment.ID, RevisionID: deployment.RevisionID, }, nil } @@ -124,7 +134,7 @@ func RunHelmUpgrade( namespace string, deployment api.KubernetesAgentDeployment, ) (*AgentDeployment, error) { - cfg, err := GetHelmActionConfig(ctx, namespace, &deployment.AgentDeployment) + cfg, err := GetHelmActionConfig(ctx, namespace, &deployment) if err != nil { return nil, err } @@ -136,6 +146,7 @@ func RunHelmUpgrade( upgradeAction.Wait = true upgradeAction.Atomic = true upgradeAction.Namespace = namespace + upgradeAction.PlainHTTP = agentenv.DistrRegistryPlainHTTP if chart, err := RunHelmPreflight(&upgradeAction.ChartPathOptions, deployment); err != nil { return nil, fmt.Errorf("helm preflight failed: %w", err) } else if release, err := upgradeAction.RunWithContext( @@ -145,6 +156,7 @@ func RunHelmUpgrade( return &AgentDeployment{ ReleaseName: release.Name, HelmRevision: release.Version, + ID: deployment.ID, RevisionID: deployment.RevisionID, }, nil } @@ -171,7 +183,7 @@ func GetHelmManifest( namespace string, deployment api.KubernetesAgentDeployment, ) ([]*unstructured.Unstructured, error) { - cfg, err := GetHelmActionConfig(ctx, namespace, &deployment.AgentDeployment) + cfg, err := GetHelmActionConfig(ctx, namespace, nil) if err != nil { return nil, err } @@ -183,3 +195,14 @@ func GetHelmManifest( return DecodeResourceYaml([]byte(release.Manifest)) } } + +func addImagePullSecretToValues(relaseName string, values map[string]any) { + if s, ok := values["imagePullSecrets"].([]any); ok { + values["imagePullSecrets"] = append(s, map[string]any{"name": PullSecretName(relaseName)}) + } + for _, v := range values { + if m, ok := v.(map[string]any); ok { + addImagePullSecretToValues(relaseName, m) + } + } +} diff --git a/cmd/agent/kubernetes/main.go b/cmd/agent/kubernetes/main.go index fd5e37635..6a54cc2df 100644 --- a/cmd/agent/kubernetes/main.go +++ b/cmd/agent/kubernetes/main.go @@ -10,48 +10,39 @@ import ( "time" "github.com/glasskube/distr/api" + "github.com/glasskube/distr/internal/agentauth" "github.com/glasskube/distr/internal/agentclient" + "github.com/glasskube/distr/internal/agentenv" "github.com/glasskube/distr/internal/types" "github.com/glasskube/distr/internal/util" "github.com/google/uuid" "go.uber.org/zap" "helm.sh/helm/v3/pkg/storage/driver" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" + applyconfigurationscorev1 "k8s.io/client-go/applyconfigurations/core/v1" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" ) var ( - interval = 5 * time.Second logger = util.Require(zap.NewDevelopment()) agentClient = util.Require(agentclient.NewFromEnv(logger)) k8sConfigFlags = genericclioptions.NewConfigFlags(true) k8sClient = util.Require(kubernetes.NewForConfig(util.Require(k8sConfigFlags.ToRESTConfig()))) k8sDynamicClient = util.Require(dynamic.NewForConfig(util.Require(k8sConfigFlags.ToRESTConfig()))) k8sRestMapper = util.Require(k8sConfigFlags.ToRESTMapper()) - agentVersionId = os.Getenv("DISTR_AGENT_VERSION_ID") ) func init() { - if intervalStr, ok := os.LookupEnv("DISTR_INTERVAL"); ok { - interval = util.Require(time.ParseDuration(intervalStr)) - } - if agentVersionId == "" { - logger.Warn("DISTR_AGENT_VERSION_ID is not set. self updates will be disabled") + if agentenv.AgentVersionID == "" { + logger.Warn("AgentVersionID is not set. self updates will be disabled") } } func main() { - ctx, cancel := context.WithCancel(context.Background()) - go func() { - sigint := make(chan os.Signal, 1) - signal.Notify(sigint, syscall.SIGTERM, syscall.SIGINT) - <-sigint - logger.Info("received termination signal") - cancel() - }() - tick := time.Tick(interval) - + ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) + tick := time.Tick(agentenv.Interval) for ctx.Err() == nil { select { case <-tick: @@ -111,12 +102,12 @@ func main() { progressCtx, progressCancel := context.WithCancel(ctx) go func(ctx context.Context) { - tick := time.Tick(interval) + tick := time.Tick(agentenv.Interval) for { select { - case <-progressCtx.Done(): + case <-ctx.Done(): logger.Info("stop sending progress updates") - break + return case <-tick: logger.Info("sending progress update") pushProgressingStatus(ctx, *res.Deployment) @@ -133,8 +124,8 @@ func main() { } func runSelfUpdateIfNeeded(ctx context.Context, namespace string, targetVersion types.AgentVersion) bool { - if agentVersionId != "" { - if agentVersionId != targetVersion.ID.String() { + if agentenv.AgentVersionID != "" { + if agentenv.AgentVersionID != targetVersion.ID.String() { logger.Info("agent version has changed. starting self-update") if manifest, err := agentClient.Manifest(ctx); err != nil { logger.Error("error fetching agent manifest", zap.Error(err)) @@ -177,6 +168,14 @@ func runInstallOrUpgrade( deployment api.KubernetesAgentDeployment, currentDeployment *AgentDeployment, ) { + if _, err := agentauth.EnsureAuth(ctx, agentClient.RawToken(), deployment.AgentDeployment); err != nil { + logger.Error("failed to ensure docker auth", zap.Error(err)) + pushErrorStatus(ctx, deployment, fmt.Errorf("failed to ensure docker auth: %w", err)) + } else if err := ensureImagePullSecret(ctx, namespace, deployment); err != nil { + logger.Error("failed to ensure image pull secret", zap.Error(err)) + pushErrorStatus(ctx, deployment, fmt.Errorf("failed to ensure image pull secret: %w", err)) + } + if currentDeployment == nil { if installedDeployment, err := RunHelmInstall(ctx, namespace, deployment); err != nil { logger.Error("helm upgrade failed", zap.Error(err)) @@ -240,8 +239,34 @@ func pushProgressingStatus(ctx context.Context, deployment api.KubernetesAgentDe } } -func pushErrorStatus(ctx context.Context, deployment api.KubernetesAgentDeployment, error error) { - if err := agentClient.Status(ctx, deployment.RevisionID, types.DeploymentStatusTypeError, error.Error()); err != nil { +func pushErrorStatus(ctx context.Context, deployment api.KubernetesAgentDeployment, err error) { + if err := agentClient.Status(ctx, deployment.RevisionID, types.DeploymentStatusTypeError, err.Error()); err != nil { logger.Warn("status push failed", zap.Error(err)) } } + +func ensureImagePullSecret(ctx context.Context, namespace string, deployment api.KubernetesAgentDeployment) error { + // It's easiest to simply copy the docker config from the file previously created by [agentauth.EnsureAuth]. + // However, be aware that this will not work when running the angent locally when a docker credential helper is + // installed. + dockerConfigPath := agentauth.DockerConfigPath(deployment.AgentDeployment) + dockerConfigData, err := os.ReadFile(dockerConfigPath) + if err != nil { + return fmt.Errorf("failed to read docker config from %v: %w", dockerConfigPath, err) + } + secretName := PullSecretName(deployment.ReleaseName) + secretCfg := applyconfigurationscorev1.Secret(secretName, namespace) + secretCfg.WithType("kubernetes.io/dockerconfigjson") + secretCfg.WithData(map[string][]byte{ + ".dockerconfigjson": dockerConfigData, + }) + _, err = k8sClient.CoreV1().Secrets(namespace).Apply( + ctx, + secretCfg, + metav1.ApplyOptions{Force: true, FieldManager: "distr-agent"}, + ) + if err != nil { + return fmt.Errorf("failed to apply secret resource %v: %w", secretName, err) + } + return nil +} diff --git a/go.mod b/go.mod index 39472adf1..448122229 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.17.67 github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2 github.com/aws/aws-sdk-go-v2/service/ses v1.30.2 + github.com/containerd/log v0.1.0 github.com/docker/cli v28.0.4+incompatible github.com/getsentry/sentry-go v0.32.0 github.com/go-chi/chi/v5 v5.2.1 @@ -33,11 +34,6 @@ require ( oras.land/oras-go v1.2.6 ) -require ( - github.com/klauspost/cpuid/v2 v2.2.10 // indirect - github.com/zeebo/xxh3 v1.0.2 // indirect -) - require ( dario.cat/mergo v1.0.1 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect @@ -69,7 +65,6 @@ require ( github.com/chai2010/gettext-go v1.0.2 // indirect github.com/containerd/containerd v1.7.27 // indirect github.com/containerd/errdefs v0.3.0 // indirect - github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/cyphar/filepath-securejoin v0.3.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -118,6 +113,7 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect @@ -162,6 +158,7 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect go.opentelemetry.io/otel v1.33.0 // indirect diff --git a/internal/agentauth/auth.go b/internal/agentauth/auth.go index 197655b27..998adbd45 100644 --- a/internal/agentauth/auth.go +++ b/internal/agentauth/auth.go @@ -7,8 +7,10 @@ import ( "os" "path" + containerdlog "github.com/containerd/log" dockerconfig "github.com/docker/cli/cli/config" "github.com/glasskube/distr/api" + "github.com/glasskube/distr/internal/agentenv" "github.com/google/uuid" "oras.land/oras-go/pkg/auth" dockerauth "oras.land/oras-go/pkg/auth/docker" @@ -17,7 +19,15 @@ import ( var previousAuth = map[uuid.UUID]map[string]api.AgentRegistryAuth{} var authClients = map[uuid.UUID]auth.Client{} -func EnsureAuth(ctx context.Context, deployment api.AgentDeployment) (auth.Client, error) { +func init() { + _ = containerdlog.SetLevel("warn") +} + +func EnsureAuth( + ctx context.Context, + jwt string, + deployment api.AgentDeployment, +) (auth.Client, error) { if err := os.MkdirAll(DockerConfigDir(deployment), 0o700); err != nil { return nil, fmt.Errorf("could not create docker config dir for deployment: %w", err) } @@ -34,6 +44,21 @@ func EnsureAuth(ctx context.Context, deployment api.AgentDeployment) (auth.Clien } } + if agentenv.DistrRegistryHost != "" { + opts := []auth.LoginOption{ + auth.WithLoginContext(ctx), + auth.WithLoginHostname(agentenv.DistrRegistryHost), + auth.WithLoginUsername("unused"), + auth.WithLoginSecret(jwt), + } + if agentenv.DistrRegistryPlainHTTP { + opts = append(opts, auth.WithLoginInsecure()) + } + if err := client.LoginWithOpts(opts...); err != nil { + return nil, fmt.Errorf("docker login failed for %v: %w", agentenv.DistrRegistryHost, err) + } + } + if !maps.Equal(previousAuth[deployment.ID], deployment.RegistryAuth) { for url, registry := range deployment.RegistryAuth { if err := client.LoginWithOpts( @@ -69,11 +94,3 @@ func DockerConfigDir(deployment api.AgentDeployment) string { func DockerConfigPath(deployment api.AgentDeployment) string { return path.Join(DockerConfigDir(deployment), dockerconfig.ConfigFileName) } - -func DockerConfigEnv(deployment api.AgentDeployment) []string { - if len(deployment.RegistryAuth) > 0 { - return []string{dockerconfig.EnvOverrideConfigDir + "=" + DockerConfigDir(deployment)} - } else { - return nil - } -} diff --git a/internal/agentclient/client.go b/internal/agentclient/client.go index ccdd91c4e..0db27ff17 100644 --- a/internal/agentclient/client.go +++ b/internal/agentclient/client.go @@ -153,6 +153,10 @@ func (c *Client) HasTokenExpiredAfter(t time.Time) bool { return c.token == nil || c.token.Expiration().Before(t) } +func (c *Client) RawToken() string { + return c.rawToken +} + func (c *Client) doAuthenticated(ctx context.Context, r *http.Request) (*http.Response, error) { if err := c.EnsureToken(ctx); err != nil { return nil, err diff --git a/internal/agentenv/env.go b/internal/agentenv/env.go new file mode 100644 index 000000000..a995b4182 --- /dev/null +++ b/internal/agentenv/env.go @@ -0,0 +1,16 @@ +package agentenv + +import ( + "strconv" + "time" + + "github.com/glasskube/distr/internal/envparse" + "github.com/glasskube/distr/internal/envutil" +) + +var ( + AgentVersionID = envutil.GetEnv("DISTR_AGENT_VERSION_ID") + Interval = envutil.GetEnvParsedOrDefault("DISTR_INTERVAL", envparse.PositiveDuration, 5*time.Second) + DistrRegistryHost = envutil.GetEnv("DISTR_REGISTRY_HOST") + DistrRegistryPlainHTTP = envutil.GetEnvParsedOrDefault("DISTR_REGISTRY_PLAIN_HTTP", strconv.ParseBool, false) +) diff --git a/internal/agentmanifest/common.go b/internal/agentmanifest/common.go index 081b77e55..373e0ba50 100644 --- a/internal/agentmanifest/common.go +++ b/internal/agentmanifest/common.go @@ -48,6 +48,8 @@ func getTemplateData( ) map[string]any { result := map[string]any{ "agentInterval": env.AgentInterval(), + "registryEnabled": env.RegistryEnabled(), + "registryHost": env.RegistryHost(), "agentDockerConfig": base64.StdEncoding.EncodeToString(env.AgentDockerConfig()), "agentVersion": deploymentTarget.AgentVersion.Name, "agentVersionId": deploymentTarget.AgentVersion.ID, @@ -72,7 +74,7 @@ func getTemplate(deploymentTarget types.DeploymentTargetWithCreatedBy) (*templat return resources.GetTemplate(path.Join( "agent/docker", deploymentTarget.AgentVersion.ComposeFileRevision, - "docker-compose.yaml", + "docker-compose.yaml.tmpl", )) } else { return resources.GetTemplate(path.Join( diff --git a/internal/auth/authentication.go b/internal/auth/authentication.go index 8421ae99b..2cd081e2e 100644 --- a/internal/auth/authentication.go +++ b/internal/auth/authentication.go @@ -44,14 +44,25 @@ var AgentAuthentication = authn.New( // ArtifactsAuthentication supports Basic auth login for OCI clients, where the password should be a PAT. // The given PAT is verified against the database, to make sure that the user still exists. var ArtifactsAuthentication = authn.New( - authn.Chain4( + authn.Chain( token.NewExtractor( token.WithExtractorFuncs(token.FromBasicAuth()), - token.WithErrorHeaders(map[string]string{"WWW-Authenticate": "Basic realm=\"Distr\""}), + token.WithErrorHeaders(http.Header{"WWW-Authenticate": []string{"Basic realm=\"Distr\""}}), + ), + authn.Alternative( + // Authenticate UserAccount with PAT + authn.Chain3( + authkey.Authenticator(), + authinfo.AuthKeyAuthenticator(), + authinfo.DbAuthenticator(), + ), + // Authenticate with Agent JWT + authn.Chain3( + jwt.Authenticator(authjwt.JWTAuth), + authinfo.AgentJWTAuthenticator(), + authinfo.AgentDbAuthenticator(), + ), ), - authkey.Authenticator(), - authinfo.AuthKeyAuthenticator(), - authinfo.DbAuthenticator(), ), ) diff --git a/internal/authn/authentication.go b/internal/authn/authentication.go index 106c90722..7aaecc2a8 100644 --- a/internal/authn/authentication.go +++ b/internal/authn/authentication.go @@ -4,6 +4,8 @@ import ( "context" "errors" "net/http" + + "go.uber.org/multierr" ) type contextKey struct{} @@ -76,16 +78,25 @@ func (a *Authentication[T]) ValidatorMiddleware(fn func(value T) error) func(nex } func (a *Authentication[T]) handleError(w http.ResponseWriter, r *http.Request, err error) { - hhe := &HttpHeaderError{} - if errors.As(err, &hhe) { - hhe.WriteTo(w) + for _, err := range multierr.Errors(err) { + var rh WithResponseHeaders + if errors.As(err, &rh) { + for key, value := range rh.ResponseHeaders() { + for _, v := range value { + w.Header().Add(key, v) + } + } + } } + statusCode := http.StatusInternalServerError + if errors.Is(err, ErrBadAuthentication) || errors.Is(err, ErrNoAuthentication) { - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - } else if a.unknownErrorHandler == nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } else { + statusCode = http.StatusUnauthorized + } else if a.unknownErrorHandler != nil { a.unknownErrorHandler(w, r, err) + return } + + http.Error(w, http.StatusText(statusCode), statusCode) } diff --git a/internal/authn/authenticator.go b/internal/authn/authenticator.go index 7421d5743..0a4dc6e57 100644 --- a/internal/authn/authenticator.go +++ b/internal/authn/authenticator.go @@ -2,7 +2,10 @@ package authn import ( "context" + "errors" "net/http" + + "go.uber.org/multierr" ) type Authenticator[IN any, OUT any] interface { @@ -61,3 +64,23 @@ func Chain4[IN any, MID1 any, MID2 any, MID3 any, OUT any]( } }) } + +func Alternative[A any, B any](authenticators ...Authenticator[A, B]) Authenticator[A, B] { + return AuthenticatorFunc[A, B](func(ctx context.Context, in A) (result B, err error) { + for _, authenticator := range authenticators { + if out, err1 := authenticator.Authenticate(ctx, in); err1 != nil { + multierr.AppendInto(&err, err1) + if errors.Is(err1, ErrNoAuthentication) || errors.Is(err1, ErrBadAuthentication) { + continue + } else { + break + } + } else { + result = out + err = nil + break + } + } + return + }) +} diff --git a/internal/authn/authinfo/db.go b/internal/authn/authinfo/db.go index 7e30febe7..bdc706627 100644 --- a/internal/authn/authinfo/db.go +++ b/internal/authn/authinfo/db.go @@ -8,6 +8,7 @@ import ( "github.com/glasskube/distr/internal/authn" "github.com/glasskube/distr/internal/db" "github.com/glasskube/distr/internal/types" + "github.com/glasskube/distr/internal/util" ) type DbAuthInfo struct { @@ -48,3 +49,27 @@ func DbAuthenticator() authn.Authenticator[AuthInfo, *DbAuthInfo] { }, nil }) } + +func AgentDbAuthenticator() authn.Authenticator[AgentAuthInfo, *DbAuthInfo] { + fn := func(ctx context.Context, a AgentAuthInfo) (*DbAuthInfo, error) { + userWithRole, org, err := db.GetUserAccountAndOrgForDeploymentTarget(ctx, a.CurrentDeploymentTargetID()) + if errors.Is(err, apierrors.ErrNotFound) { + return nil, authn.ErrBadAuthentication + } else if err != nil { + return nil, err + } + return &DbAuthInfo{ + AuthInfo: &SimpleAuthInfo{ + organizationID: &org.ID, + userID: userWithRole.ID, + userEmail: userWithRole.Email, + emailVerified: userWithRole.EmailVerifiedAt != nil, + userRole: util.PtrTo(userWithRole.UserRole), + rawToken: a.Token(), + }, + user: util.PtrTo(userWithRole.AsUserAccount()), + org: org, + }, nil + } + return authn.AuthenticatorFunc[AgentAuthInfo, *DbAuthInfo](fn) +} diff --git a/internal/authn/errors.go b/internal/authn/errors.go index 2ab5a53b8..bd1579ce1 100644 --- a/internal/authn/errors.go +++ b/internal/authn/errors.go @@ -15,10 +15,17 @@ var ErrBadAuthentication = errors.New("bad authentication") type HttpHeaderError struct { wrapped error - headers map[string]string + headers http.Header } -func NewHttpHeaderError(err error, headers map[string]string) error { +// ResponseHEaders implements WithResponseHeaders. +func (err *HttpHeaderError) ResponseHeaders() http.Header { + return err.headers +} + +var _ WithResponseHeaders = &HttpHeaderError{} + +func NewHttpHeaderError(err error, headers http.Header) error { return &HttpHeaderError{ wrapped: err, headers: headers, @@ -32,9 +39,3 @@ func (err *HttpHeaderError) Error() string { func (err *HttpHeaderError) Unwrap() error { return err.wrapped } - -func (err *HttpHeaderError) WriteTo(w http.ResponseWriter) { - for k, v := range err.headers { - w.Header().Set(k, v) - } -} diff --git a/internal/authn/response.go b/internal/authn/response.go new file mode 100644 index 000000000..37ea8759a --- /dev/null +++ b/internal/authn/response.go @@ -0,0 +1,9 @@ +package authn + +import ( + "net/http" +) + +type WithResponseHeaders interface { + ResponseHeaders() http.Header +} diff --git a/internal/authn/token/token.go b/internal/authn/token/token.go index dbd22958c..1eb4f5c2b 100644 --- a/internal/authn/token/token.go +++ b/internal/authn/token/token.go @@ -12,7 +12,7 @@ type TokenExtractorFunc func(r *http.Request) string type TokenExtractor struct { fns []TokenExtractorFunc - headers map[string]string + headers http.Header } // Authenticate implements Provider. @@ -35,7 +35,7 @@ func WithExtractorFuncs(fns ...TokenExtractorFunc) ExtractorOption { } } -func WithErrorHeaders(headers map[string]string) ExtractorOption { +func WithErrorHeaders(headers http.Header) ExtractorOption { return func(te *TokenExtractor) { te.headers = headers } diff --git a/internal/db/organization.go b/internal/db/organization.go index 281d2381f..cf0d0f779 100644 --- a/internal/db/organization.go +++ b/internal/db/organization.go @@ -58,7 +58,7 @@ func UpdateOrganization(ctx context.Context, org *types.Organization) error { } } -func GetOrganizationsForUser(ctx context.Context, userID uuid.UUID) ([]*types.OrganizationWithUserRole, error) { +func GetOrganizationsForUser(ctx context.Context, userID uuid.UUID) ([]types.OrganizationWithUserRole, error) { db := internalctx.GetDb(ctx) rows, err := db.Query(ctx, ` SELECT`+organizationOutputExpr+`, j.user_role @@ -70,7 +70,7 @@ func GetOrganizationsForUser(ctx context.Context, userID uuid.UUID) ([]*types.Or if err != nil { return nil, err } - result, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[types.OrganizationWithUserRole]) + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[types.OrganizationWithUserRole]) if err != nil { return nil, err } else { diff --git a/internal/db/user_accounts.go b/internal/db/user_accounts.go index 65f4c5499..185d250af 100644 --- a/internal/db/user_accounts.go +++ b/internal/db/user_accounts.go @@ -241,7 +241,8 @@ func GetUserAccountWithRole(ctx context.Context, userID, orgID uuid.UUID) (*type func GetUserAccountAndOrg(ctx context.Context, userID, orgID uuid.UUID, role types.UserRole) ( *types.UserAccount, - *types.Organization, error, + *types.Organization, + error, ) { db := internalctx.GetDb(ctx) rows, err := db.Query(ctx, @@ -271,6 +272,40 @@ func GetUserAccountAndOrg(ctx context.Context, userID, orgID uuid.UUID, role typ } } +func GetUserAccountAndOrgForDeploymentTarget( + ctx context.Context, + id uuid.UUID, +) (*types.UserAccountWithUserRole, *types.Organization, error) { + db := internalctx.GetDb(ctx) + rows, err := db.Query(ctx, + "SELECT ("+userAccountWithRoleOutputExpr+`), + (`+organizationOutputExpr+`) + FROM DeploymentTarget dt + JOIN Organization o ON o.id = dt.organization_id + JOIN UserAccount u ON u.id = dt.created_by_user_account_id + JOIN Organization_UserAccount j ON u.id = j.user_account_id + AND o.id = j.organization_id + WHERE dt.id = @id`, + pgx.NamedArgs{"id": id}, + ) + if err != nil { + return nil, nil, err + } + res, err := pgx.CollectExactlyOneRow[struct { + User types.UserAccountWithUserRole + Org types.Organization + }](rows, pgx.RowToStructByPos) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, nil, apierrors.ErrNotFound + } else { + return nil, nil, fmt.Errorf("could not map user or org: %w", err) + } + } else { + return &res.User, &res.Org, nil + } +} + func UpdateUserAccountLastLoggedIn(ctx context.Context, userID uuid.UUID) error { db := internalctx.GetDb(ctx) cmd, err := db.Exec( diff --git a/internal/env/env.go b/internal/env/env.go index 3c6e11bc0..13e6f82d2 100644 --- a/internal/env/env.go +++ b/internal/env/env.go @@ -7,6 +7,8 @@ import ( "strconv" "time" + "github.com/glasskube/distr/internal/envparse" + "github.com/glasskube/distr/internal/envutil" "github.com/joho/godotenv" ) @@ -45,56 +47,58 @@ func init() { } } - databaseUrl = requireEnv("DATABASE_URL") - jwtSecret = requireEnvParsed("JWT_SECRET", base64.StdEncoding.DecodeString) - host = requireEnv("DISTR_HOST") - agentInterval = getEnvParsedOrDefault("AGENT_INTERVAL", getPositiveDuration, 5*time.Second) - statusEntriesMaxAge = getEnvParsedOrNil("STATUS_ENTRIES_MAX_AGE", getPositiveDuration) - enableQueryLogging = getEnvParsedOrDefault("ENABLE_QUERY_LOGGING", strconv.ParseBool, false) + databaseUrl = envutil.RequireEnv("DATABASE_URL") + jwtSecret = envutil.RequireEnvParsed("JWT_SECRET", base64.StdEncoding.DecodeString) + host = envutil.RequireEnv("DISTR_HOST") + agentInterval = envutil.GetEnvParsedOrDefault("AGENT_INTERVAL", envparse.PositiveDuration, 5*time.Second) + statusEntriesMaxAge = envutil.GetEnvParsedOrNil("STATUS_ENTRIES_MAX_AGE", envparse.PositiveDuration) + enableQueryLogging = envutil.GetEnvParsedOrDefault("ENABLE_QUERY_LOGGING", strconv.ParseBool, false) userEmailVerificationRequired = - getEnvParsedOrDefault("USER_EMAIL_VERIFICATION_REQUIRED", strconv.ParseBool, true) - serverShutdownDelayDuration = getEnvParsedOrNil("SERVER_SHUTDOWN_DELAY_DURATION", getPositiveDuration) - registration = getEnvParsedOrDefault("REGISTRATION", parseRegistrationMode, RegistrationEnabled) + envutil.GetEnvParsedOrDefault("USER_EMAIL_VERIFICATION_REQUIRED", strconv.ParseBool, true) + serverShutdownDelayDuration = envutil.GetEnvParsedOrNil("SERVER_SHUTDOWN_DELAY_DURATION", envparse.PositiveDuration) + registration = envutil.GetEnvParsedOrDefault("REGISTRATION", parseRegistrationMode, RegistrationEnabled) inviteTokenValidDuration = - getEnvParsedOrDefault("INVITE_TOKEN_VALID_DURATION", getPositiveDuration, 24*time.Hour) + envutil.GetEnvParsedOrDefault("INVITE_TOKEN_VALID_DURATION", envparse.PositiveDuration, 24*time.Hour) resetTokenValidDuration = - getEnvParsedOrDefault("RESET_TOKEN_VALID_DURATION", getPositiveDuration, 1*time.Hour) + envutil.GetEnvParsedOrDefault("RESET_TOKEN_VALID_DURATION", envparse.PositiveDuration, 1*time.Hour) agentTokenMaxValidDuration = - getEnvParsedOrDefault("AGENT_TOKEN_MAX_VALID_DURATION", getPositiveDuration, 24*time.Hour) + envutil.GetEnvParsedOrDefault("AGENT_TOKEN_MAX_VALID_DURATION", envparse.PositiveDuration, 24*time.Hour) - mailerConfig.Type = getEnvParsedOrDefault("MAILER_TYPE", parseMailerType, MailerTypeUnspecified) + mailerConfig.Type = envutil.GetEnvParsedOrDefault("MAILER_TYPE", parseMailerType, MailerTypeUnspecified) if mailerConfig.Type != MailerTypeUnspecified { - mailerConfig.FromAddress = requireEnvParsed("MAILER_FROM_ADDRESS", parseMailAddress) + mailerConfig.FromAddress = envutil.RequireEnvParsed("MAILER_FROM_ADDRESS", envparse.MailAddress) } if mailerConfig.Type == MailerTypeSMTP { mailerConfig.SmtpConfig = &MailerSMTPConfig{ - Host: getEnv("MAILER_SMTP_HOST"), - Port: requireEnvParsed("MAILER_SMTP_PORT", strconv.Atoi), - Username: getEnv("MAILER_SMTP_USERNAME"), - Password: getEnv("MAILER_SMTP_PASSWORD"), + Host: envutil.GetEnv("MAILER_SMTP_HOST"), + Port: envutil.RequireEnvParsed("MAILER_SMTP_PORT", strconv.Atoi), + Username: envutil.GetEnv("MAILER_SMTP_USERNAME"), + Password: envutil.GetEnv("MAILER_SMTP_PASSWORD"), } } - registryEnabled = getEnvParsedOrDefault("REGISTRY_ENABLED", strconv.ParseBool, false) + registryEnabled = envutil.GetEnvParsedOrDefault("REGISTRY_ENABLED", strconv.ParseBool, false) if registryEnabled { - registryHost = getEnvOrDefault("REGISTRY_HOST", host, getEnvOpts{deprecatedAlias: "DISTR_ARTIFACTS_HOST"}) - registryS3Config.Bucket = requireEnv("REGISTRY_S3_BUCKET") - registryS3Config.Region = requireEnv("REGISTRY_S3_REGION") - registryS3Config.Endpoint = getEnvOrNil("REGISTRY_S3_ENDPOINT") - registryS3Config.AccessKeyID = getEnvOrNil("REGISTRY_S3_ACCESS_KEY_ID") - registryS3Config.SecretAccessKey = getEnvOrNil("REGISTRY_S3_SECRET_ACCESS_KEY") - registryS3Config.UsePathStyle = getEnvParsedOrDefault("REGISTRY_S3_USE_PATH_STYLE", strconv.ParseBool, false) - registryS3Config.AllowRedirect = getEnvParsedOrDefault("REGISTRY_S3_ALLOW_REDIRECT", strconv.ParseBool, true) + registryHost = + envutil.GetEnvOrDefault("REGISTRY_HOST", host, envutil.GetEnvOpts{DeprecatedAlias: "DISTR_ARTIFACTS_HOST"}) + registryS3Config.Bucket = envutil.RequireEnv("REGISTRY_S3_BUCKET") + registryS3Config.Region = envutil.RequireEnv("REGISTRY_S3_REGION") + registryS3Config.Endpoint = envutil.GetEnvOrNil("REGISTRY_S3_ENDPOINT") + registryS3Config.AccessKeyID = envutil.GetEnvOrNil("REGISTRY_S3_ACCESS_KEY_ID") + registryS3Config.SecretAccessKey = envutil.GetEnvOrNil("REGISTRY_S3_SECRET_ACCESS_KEY") + registryS3Config.UsePathStyle = envutil.GetEnvParsedOrDefault("REGISTRY_S3_USE_PATH_STYLE", strconv.ParseBool, false) + registryS3Config.AllowRedirect = envutil.GetEnvParsedOrDefault("REGISTRY_S3_ALLOW_REDIRECT", strconv.ParseBool, true) } - artifactTagsDefaultLimitPerOrg = getEnvParsedOrDefault("ARTIFACT_TAGS_DEFAULT_LIMIT_PER_ORG", getNonNegativeNumber, 0) - - sentryDSN = getEnv("SENTRY_DSN") - sentryDebug = getEnvParsedOrDefault("SENTRY_DEBUG", strconv.ParseBool, false) - agentDockerConfig = getEnvParsedOrDefault("AGENT_DOCKER_CONFIG", asByteSlice, nil) - frontendSentryDSN = getEnvOrNil("FRONTEND_SENTRY_DSN") - frontendPosthogToken = getEnvOrNil("FRONTEND_POSTHOG_TOKEN") - frontendPosthogAPIHost = getEnvOrNil("FRONTEND_POSTHOG_API_HOST") - frontendPosthogUIHost = getEnvOrNil("FRONTEND_POSTHOG_UI_HOST") + artifactTagsDefaultLimitPerOrg = + envutil.GetEnvParsedOrDefault("ARTIFACT_TAGS_DEFAULT_LIMIT_PER_ORG", envparse.NonNegativeNumber, 0) + + sentryDSN = envutil.GetEnv("SENTRY_DSN") + sentryDebug = envutil.GetEnvParsedOrDefault("SENTRY_DEBUG", strconv.ParseBool, false) + agentDockerConfig = envutil.GetEnvParsedOrDefault("AGENT_DOCKER_CONFIG", envparse.ByteSlice, nil) + frontendSentryDSN = envutil.GetEnvOrNil("FRONTEND_SENTRY_DSN") + frontendPosthogToken = envutil.GetEnvOrNil("FRONTEND_POSTHOG_TOKEN") + frontendPosthogAPIHost = envutil.GetEnvOrNil("FRONTEND_POSTHOG_API_HOST") + frontendPosthogUIHost = envutil.GetEnvOrNil("FRONTEND_POSTHOG_UI_HOST") } func DatabaseUrl() string { diff --git a/internal/env/parse.go b/internal/envparse/envparse.go similarity index 69% rename from internal/env/parse.go rename to internal/envparse/envparse.go index a7c0257bd..4a24f3509 100644 --- a/internal/env/parse.go +++ b/internal/envparse/envparse.go @@ -1,4 +1,4 @@ -package env +package envparse import ( "errors" @@ -7,7 +7,7 @@ import ( "time" ) -func getPositiveDuration(value string) (time.Duration, error) { +func PositiveDuration(value string) (time.Duration, error) { parsed, err := time.ParseDuration(value) if err == nil && parsed.Nanoseconds() <= 0 { err = errors.New("duration must be positive") @@ -15,11 +15,11 @@ func getPositiveDuration(value string) (time.Duration, error) { return parsed, err } -func asByteSlice(s string) ([]byte, error) { +func ByteSlice(s string) ([]byte, error) { return []byte(s), nil } -func parseMailAddress(s string) (mail.Address, error) { +func MailAddress(s string) (mail.Address, error) { if parsed, err := mail.ParseAddress(s); err != nil || parsed == nil { return mail.Address{}, err } else { @@ -27,7 +27,7 @@ func parseMailAddress(s string) (mail.Address, error) { } } -func getNonNegativeNumber(value string) (int, error) { +func NonNegativeNumber(value string) (int, error) { parsed, err := strconv.Atoi(value) if err == nil && parsed < 0 { err = errors.New("number must not be negative") diff --git a/internal/env/util.go b/internal/envutil/util.go similarity index 59% rename from internal/env/util.go rename to internal/envutil/util.go index b22788c0b..e110a071a 100644 --- a/internal/env/util.go +++ b/internal/envutil/util.go @@ -1,39 +1,39 @@ -package env +package envutil import ( "fmt" "os" ) -type getEnvOpts struct { - deprecatedAlias string +type GetEnvOpts struct { + DeprecatedAlias string } -func getEnv(key string) string { +func GetEnv(key string) string { return os.Getenv(key) } -func getEnvOrNil(key string) *string { +func GetEnvOrNil(key string) *string { if value, ok := os.LookupEnv(key); ok { return &value } return nil } -func getEnvOrDefault(key, defaultValue string, opts getEnvOpts) string { - if value := getEnv(key); value != "" { +func GetEnvOrDefault(key, defaultValue string, opts GetEnvOpts) string { + if value := GetEnv(key); value != "" { return value - } else if opts.deprecatedAlias != "" { - if value := getEnv(opts.deprecatedAlias); value != "" { + } else if opts.DeprecatedAlias != "" { + if value := GetEnv(opts.DeprecatedAlias); value != "" { fmt.Fprintf(os.Stderr, "\nWARNING: use of deprecated variable \"%v\", please use \"%v\" instead\n\n", - opts.deprecatedAlias, key) + opts.DeprecatedAlias, key) return value } } return defaultValue } -func getEnvParsedOrNil[T any](key string, parseFunc func(string) (T, error)) *T { +func GetEnvParsedOrNil[T any](key string, parseFunc func(string) (T, error)) *T { if value, ok := os.LookupEnv(key); ok { if parsed, err := parseFunc(value); err != nil { panic(fmt.Sprintf("malformed environment variable %v: %v", key, err)) @@ -44,7 +44,7 @@ func getEnvParsedOrNil[T any](key string, parseFunc func(string) (T, error)) *T return nil } -func getEnvParsedOrDefault[T any](key string, parseFunc func(string) (T, error), defaultValue T) T { +func GetEnvParsedOrDefault[T any](key string, parseFunc func(string) (T, error), defaultValue T) T { if value, ok := os.LookupEnv(key); ok { if parsed, err := parseFunc(value); err != nil { panic(fmt.Sprintf("malformed environment variable %v: %v", key, err)) @@ -55,15 +55,15 @@ func getEnvParsedOrDefault[T any](key string, parseFunc func(string) (T, error), return defaultValue } -func requireEnv(key string) string { - if value := getEnv(key); value != "" { +func RequireEnv(key string) string { + if value := GetEnv(key); value != "" { return value } panic(fmt.Sprintf("missing required environment variable: %v", key)) } -func requireEnvParsed[T any](key string, parseFunc func(string) (T, error)) T { - if parsed, err := parseFunc(requireEnv(key)); err != nil { +func RequireEnvParsed[T any](key string, parseFunc func(string) (T, error)) T { + if parsed, err := parseFunc(RequireEnv(key)); err != nil { panic(fmt.Sprintf("malformed environment variable %v: %v", key, err)) } else { return parsed diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index ba709c66e..15cadbf1e 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -71,7 +71,7 @@ func authLoginHandler(w http.ResponseWriter, r *http.Request) { } org := orgs[0] - if _, tokenString, err := authjwt.GenerateDefaultToken(*user, *org); err != nil { + if _, tokenString, err := authjwt.GenerateDefaultToken(*user, org); err != nil { return fmt.Errorf("token creation failed: %w", err) } else if err = db.UpdateUserAccountLastLoggedIn(ctx, user.ID); err != nil { return err diff --git a/internal/resources/embedded/agent/docker/v1/docker-compose.yaml b/internal/resources/embedded/agent/docker/v1/docker-compose.yaml.tmpl similarity index 89% rename from internal/resources/embedded/agent/docker/v1/docker-compose.yaml rename to internal/resources/embedded/agent/docker/v1/docker-compose.yaml.tmpl index 18006c77e..7c9dabd21 100644 --- a/internal/resources/embedded/agent/docker/v1/docker-compose.yaml +++ b/internal/resources/embedded/agent/docker/v1/docker-compose.yaml.tmpl @@ -14,6 +14,9 @@ services: DISTR_INTERVAL: '{{ .agentInterval }}' DISTR_AGENT_VERSION_ID: '{{ .agentVersionId }}' DISTR_AGENT_SCRATCH_DIR: /scratch + {{- if .registryEnabled }} + DISTR_REGISTRY_HOST: '{{ .registryHost }}' + {{- end }} HOST_DOCKER_CONFIG_DIR: ${HOST_DOCKER_CONFIG_DIR-${HOME}/.docker} volumes: - /var/run/docker.sock:/var/run/docker.sock diff --git a/internal/resources/embedded/agent/kubernetes/v1/manifest.yaml.tmpl b/internal/resources/embedded/agent/kubernetes/v1/manifest.yaml.tmpl index bce2ad42c..2bdd615b0 100644 --- a/internal/resources/embedded/agent/kubernetes/v1/manifest.yaml.tmpl +++ b/internal/resources/embedded/agent/kubernetes/v1/manifest.yaml.tmpl @@ -11,6 +11,9 @@ data: DISTR_STATUS_ENDPOINT: "{{ .statusEndpoint }}" DISTR_INTERVAL: "{{ .agentInterval }}" DISTR_AGENT_VERSION_ID: "{{ .agentVersionId }}" + {{- if .registryEnabled }} + DISTR_REGISTRY_HOST: "{{ .registryHost }}" + {{- end }} {{ if .targetSecret }} --- diff --git a/internal/types/user_account.go b/internal/types/user_account.go index d4f30c28c..39c219bae 100644 --- a/internal/types/user_account.go +++ b/internal/types/user_account.go @@ -1,8 +1,10 @@ package types import ( + "slices" "time" + "github.com/glasskube/distr/internal/util" "github.com/google/uuid" ) @@ -28,4 +30,18 @@ type UserAccountWithUserRole struct { Name string `db:"name" json:"name,omitempty"` UserRole UserRole `db:"user_role" json:"userRole"` // not copy+pasted Password string `db:"-" json:"-"` + // Don't forget to update AsUserAccount when adding fields! +} + +func (u *UserAccountWithUserRole) AsUserAccount() UserAccount { + return UserAccount{ + ID: u.ID, + CreatedAt: u.CreatedAt, + Email: u.Email, + EmailVerifiedAt: util.PtrCopy(u.EmailVerifiedAt), + PasswordHash: slices.Clone(u.PasswordHash), + PasswordSalt: slices.Clone(u.PasswordSalt), + Name: u.Name, + Password: u.Password, + } } diff --git a/internal/util/pointer.go b/internal/util/pointer.go index fba629c1d..0a95427eb 100644 --- a/internal/util/pointer.go +++ b/internal/util/pointer.go @@ -3,3 +3,11 @@ package util func PtrTo[T any](value T) *T { return &value } + +func PtrCopy[T any](ptr *T) *T { + if ptr == nil { + return nil + } + v := *ptr + return &v +}