Skip to content

Commit

Permalink
feat: send merged values to kubernetes agent (#263)
Browse files Browse the repository at this point in the history
Signed-off-by: Jakob Steiner <jakob.steiner@glasskube.eu>
  • Loading branch information
kosmoz authored Jan 13, 2025
1 parent dfbdf3a commit 969e554
Show file tree
Hide file tree
Showing 15 changed files with 172 additions and 48 deletions.
6 changes: 3 additions & 3 deletions cmd/agent/kubernetes/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ func main() {
}
} else if currentDeployment != nil {
if currentDeployment.HelmRevision != latestRelease.Version {
msg := "helm revision is different from latest deployment. bailing out"
msg := fmt.Sprintf("actual helm revision (%v) is different from latest deployed by agent (%v). bailing out",
latestRelease.Version, currentDeployment.HelmRevision)
logger.Warn(msg)
pushErrorStatus(ctx, msg)
continue
Expand Down Expand Up @@ -204,11 +205,10 @@ func GetLatestHelmRelease(namespace, releaseName string) (*release.Release, erro
return nil, err
}
historyAction := action.NewHistory(cfg)
historyAction.Max = 1
if releases, err := historyAction.Run(releaseName); err != nil {
return nil, err
} else {
return releases[0], nil
return releases[len(releases)-1], nil
}
}

Expand Down
18 changes: 18 additions & 0 deletions cmd/cloud/generate/dummy/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ func main() {
}
util.Must(db.CreateApplicationVersion(ctx, &av))

podinfoApp := types.Application{Name: "Podinfo", OrganizationID: org.ID, Type: types.DepolymentTypeKubernetes}
util.Must(db.CreateApplication(ctx, &podinfoApp))
util.Must(db.CreateApplicationVersion(ctx, &types.ApplicationVersion{
ApplicationId: podinfoApp.ID,
Name: "6.7.1",
ChartType: util.PtrTo(types.HelmChartTypeOCI),
ChartUrl: util.PtrTo("oci://ghcr.io/stefanprodan/charts/podinfo"),
ChartVersion: util.PtrTo("6.7.1"),
ValuesFileData: []byte(
"redis:\n enabled: true\n" +
"serviceAccount:\n enabled: true\n",
),
TemplateFileData: []byte(
"replicaCount: 1 # change this if needed\n" +
"podDisruptionBudget:\n # only applied if replicaCount > 1\n maxUnavailable: 1\n",
),
}))

dt1 := types.DeploymentTargetWithCreatedBy{
CreatedBy: &types.UserAccountWithUserRole{ID: pmig.ID},
DeploymentTarget: types.DeploymentTarget{
Expand Down
16 changes: 0 additions & 16 deletions cmd/cloud/migrate/down/main.go

This file was deleted.

40 changes: 40 additions & 0 deletions cmd/cloud/migrate/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package main

import (
"context"

"github.com/glasskube/cloud/internal/migrations"
"github.com/glasskube/cloud/internal/svc"
"github.com/glasskube/cloud/internal/util"
"github.com/spf13/pflag"
)

var (
down bool
to uint
)

func init() {
pflag.BoolVar(&down, "down", down, "run all down migrations")
pflag.UintVar(&to, "to", to, "run all up/down migrations to reach specified schema revision")
pflag.Parse()
if to > 0 && down {
panic("please use --to OR --down")
}
}

func main() {
ctx := context.Background()
registry := util.Require(svc.NewDefault(ctx))
defer func() { util.Must(registry.Shutdown()) }()
if to > 0 {
registry.GetLogger().Sugar().Infof("run migrations to schema version %v", to)
util.Must(migrations.Migrate(registry.GetLogger(), to))
} else if down {
registry.GetLogger().Info("run DOWN migrations")
util.Must(migrations.Down(registry.GetLogger()))
} else {
registry.GetLogger().Info("run UP migrations")
util.Must(migrations.Up(registry.GetLogger()))
}
}
16 changes: 0 additions & 16 deletions cmd/cloud/migrate/up/main.go

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,9 @@ export class DeploymentTargetsComponent implements OnInit, AfterViewInit, OnDest
if (this.deployForm.valid) {
this.deployFormLoading = true;
const deployment = this.deployForm.value;
if (deployment.valuesYaml) {
deployment.valuesYaml = btoa(deployment.valuesYaml);
}
try {
await firstValueFrom(this.deployments.create(deployment as Deployment));
this.toast.success('Deployment saved successfully');
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.32.0
gopkg.in/yaml.v3 v3.0.1
helm.sh/helm/v3 v3.16.4
k8s.io/apimachinery v0.32.0
k8s.io/cli-runtime v0.32.0
Expand Down Expand Up @@ -168,7 +169,6 @@ require (
google.golang.org/protobuf v1.36.1 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.32.0 // indirect
k8s.io/apiextensions-apiserver v0.31.3 // indirect
k8s.io/apiserver v0.31.3 // indirect
Expand Down
5 changes: 5 additions & 0 deletions internal/agentclient/httpstatus.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package agentclient
import (
"errors"
"fmt"
"io"
"net/http"
"strings"
)

var ErrHttpStatus = errors.New("non-ok http status")
Expand All @@ -12,6 +14,9 @@ func checkStatus(r *http.Response, err error) (*http.Response, error) {
if err != nil || statusOK(r) {
return r, err
} else {
if errorBody, err := io.ReadAll(r.Body); err == nil {
return r, fmt.Errorf("%w: %v (%v)", ErrHttpStatus, r.Status, strings.TrimSpace(string(errorBody)))
}
return r, fmt.Errorf("%w: %v", ErrHttpStatus, r.Status)
}
}
Expand Down
18 changes: 16 additions & 2 deletions internal/handlers/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,14 +167,28 @@ func agentResourcesHandler(w http.ResponseWriter, r *http.Request) {
Namespace: *deploymentTarget.Namespace,
}
if deployment != nil && appVersion != nil {
// TODO: parse values yaml and merge
w.Header().Add("X-Resource-Correlation-ID", deployment.ID)
respose.Deployment = &api.KubernetesAgentDeployment{
RevisionID: deployment.ID, // TODO: Update to use DeploymentRevision.ID once implemented
ReleaseName: *deployment.ReleaseName,
ChartUrl: *appVersion.ChartUrl,
ChartVersion: *appVersion.ChartVersion,
}
if versionValues, err := appVersion.ParsedValuesFile(); err != nil {
log.Warn("parse error", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} else if deploymentValues, err := deployment.ParsedValuesFile(); err != nil {
log.Warn("parse error", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} else if merged, err := util.MergeAllRecursive(versionValues, deploymentValues); err != nil {
log.Warn("merge error", zap.Error(err))
http.Error(w, fmt.Sprintf("error merging values files: %v", err), http.StatusInternalServerError)
return
} else {
respose.Deployment.Values = merged
}
w.Header().Add("X-Resource-Correlation-ID", deployment.ID)
if *appVersion.ChartType == types.HelmChartTypeRepository {
respose.Deployment.ChartName = *appVersion.ChartName
}
Expand Down
10 changes: 10 additions & 0 deletions internal/handlers/applications.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,16 +166,26 @@ func createApplicationVersion(w http.ResponseWriter, r *http.Request) {
return
} else {
applicationVersion.ComposeFileData = data
if _, err := applicationVersion.ParsedComposeFile(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
} else {
if data, ok := readFile(w, r, "valuesfile"); !ok {
return
} else {
applicationVersion.ValuesFileData = data
if _, err := applicationVersion.ParsedValuesFile(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
if data, ok := readFile(w, r, "templatefile"); !ok {
return
} else {
// Template file is taken without parsing on purpose.
// Some uses might use a non-yaml template here.
applicationVersion.TemplateFileData = data
}
}
Expand Down
18 changes: 12 additions & 6 deletions internal/handlers/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
internalctx "github.com/glasskube/cloud/internal/context"
"github.com/glasskube/cloud/internal/db"
"github.com/glasskube/cloud/internal/types"
"github.com/glasskube/cloud/internal/util"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
)
Expand Down Expand Up @@ -43,6 +44,10 @@ func createDeployment(w http.ResponseWriter, r *http.Request) {
} else if err != nil {
log.Warn("could not get application version", zap.Error(err))
http.Error(w, "an internal error occurred", http.StatusInternalServerError)
} else if appVersion, err := db.GetApplicationVersion(ctx, deployment.ApplicationVersionId); err != nil {
http.Error(w, "application version does not exist", http.StatusBadRequest)
} else if appVersionValues, err := appVersion.ParsedValuesFile(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
} else if deploymentTarget, err := db.GetDeploymentTarget(
ctx, deployment.DeploymentTargetId, &orgId,
); errors.Is(err, apierrors.ErrNotFound) {
Expand All @@ -52,15 +57,16 @@ func createDeployment(w http.ResponseWriter, r *http.Request) {
http.Error(w, "an inernal error occurred", http.StatusInternalServerError)
} else if deploymentTarget.Type != application.Type {
http.Error(w, "application and deployment target must have the same type", http.StatusBadRequest)
} else if deploymentValues, err := deployment.ParsedValuesFile(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
} else if _, err := util.MergeAllRecursive(appVersionValues, deploymentValues); err != nil {
http.Error(w, fmt.Sprintf("values cannot be merged with base: %v", err), http.StatusBadRequest)
} else if err = db.CreateDeployment(r.Context(), &deployment); err != nil {
log.Warn("could not create deployment", zap.Error(err))
sentry.GetHubFromContext(r.Context()).CaptureException(err)
w.WriteHeader(http.StatusInternalServerError)
if _, err = fmt.Fprintln(w, err); err != nil {
log.Error("failed to write error to response", zap.Error(err))
}
} else if err = json.NewEncoder(w).Encode(deployment); err != nil {
log.Error("failed to encode json", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError)
} else {
RespondJSON(w, deployment)
}
}

Expand Down
25 changes: 21 additions & 4 deletions internal/migrations/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ import (
var fs embed.FS

type Logger struct {
*zap.Logger
*zap.SugaredLogger
}

// Printf implements migrate.Logger.
func (l *Logger) Printf(format string, v ...interface{}) {
if strings.HasPrefix(format, "error") {
l.Sugar().Errorf(format, v...)
l.Errorf(format, v...)
} else {
l.Sugar().Infof(format, v...)
l.Debugf(format, v...)
}
}

Expand Down Expand Up @@ -69,6 +69,23 @@ func Down(log *zap.Logger) (err error) {
return nil
}

func Migrate(log *zap.Logger, to uint) (err error) {
db, err := sql.Open("pgx", env.DatabaseUrl())
if err != nil {
return err
}
defer func() { multierr.AppendInto(&err, db.Close()) }()
if instance, err := getInstance(db, log); err != nil {
return err
} else if err := instance.Migrate(to); err != nil {
if !errors.Is(err, migrate.ErrNoChange) {
return err
}
log.Info("migrations completed", zap.Error(err))
}
return nil
}

func getInstance(db *sql.DB, log *zap.Logger) (*migrate.Migrate, error) {
if driver, err := postgres.WithInstance(db, &postgres.Config{}); err != nil {
return nil, err
Expand All @@ -77,7 +94,7 @@ func getInstance(db *sql.DB, log *zap.Logger) (*migrate.Migrate, error) {
} else if instance, err := migrate.NewWithInstance("", sourceInstance, "cloud", driver); err != nil {
return nil, err
} else {
instance.Log = &Logger{log}
instance.Log = &Logger{log.Sugar()}
return instance, nil
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE Deployment
ADD CONSTRAINT release_name_unique UNIQUE (deployment_target_id, release_name);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE Deployment
DROP CONSTRAINT release_name_unique;
39 changes: 39 additions & 0 deletions internal/types/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package types

import (
"errors"
"fmt"
"time"

"gopkg.in/yaml.v3"
)

type Application struct {
Expand Down Expand Up @@ -35,6 +38,33 @@ type ApplicationVersion struct {
ApplicationId string `db:"application_id" json:"applicationId"`
}

func (av ApplicationVersion) ParsedValuesFile() (result map[string]any, err error) {
if av.ValuesFileData != nil {
if err = yaml.Unmarshal(av.ValuesFileData, &result); err != nil {
err = fmt.Errorf("cannot parse ApplicationVersion values file: %w", err)
}
}
return
}

func (av ApplicationVersion) ParsedTemplateFile() (result map[string]any, err error) {
if av.TemplateFileData != nil {
if err = yaml.Unmarshal(av.TemplateFileData, &result); err != nil {
err = fmt.Errorf("cannot parse ApplicationVersion values template: %w", err)
}
}
return
}

func (av ApplicationVersion) ParsedComposeFile() (result map[string]any, err error) {
if av.ComposeFileData != nil {
if err = yaml.Unmarshal(av.ComposeFileData, &result); err != nil {
err = fmt.Errorf("cannot parse ApplicationVersion compose file: %w", err)
}
}
return
}

func (av ApplicationVersion) Validate(deplType DeploymentType) error {
if deplType == DeploymentTypeDocker {
if av.ComposeFileData == nil {
Expand Down Expand Up @@ -65,6 +95,15 @@ type Deployment struct {
ValuesYaml []byte `db:"values_yaml" json:"valuesYaml"`
}

func (d Deployment) ParsedValuesFile() (result map[string]any, err error) {
if d.ValuesYaml != nil {
if err = yaml.Unmarshal(d.ValuesYaml, &result); err != nil {
err = fmt.Errorf("cannot parse Deployment values file: %w", err)
}
}
return
}

type DeploymentWithData struct {
Deployment
ApplicationId string `db:"application_id" json:"applicationId"`
Expand Down

0 comments on commit 969e554

Please sign in to comment.