Skip to content

Commit

Permalink
Merge pull request #969 from replicatedhq/laverya/live-render-in-golang
Browse files Browse the repository at this point in the history
live render config in golang
  • Loading branch information
laverya committed Aug 20, 2020
2 parents d13ee70 + 2507052 commit 448ccf1
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 152 deletions.
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -18,7 +18,7 @@ require (
github.com/bugsnag/bugsnag-go v1.5.3 // indirect
github.com/bugsnag/panicwrap v1.2.0 // indirect
github.com/containerd/continuity v0.0.0-20200228182428-0f16d7a0959c // indirect
github.com/containers/image v3.0.2+incompatible
github.com/containers/image v3.0.2+incompatible // indirect
github.com/containers/image/v5 v5.2.0
github.com/containers/storage v1.16.2 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
Expand Down
65 changes: 0 additions & 65 deletions kotsadm/api/src/kots_app/kots_app.ts
Expand Up @@ -142,19 +142,6 @@ export class KotsApp {
return "***HIDDEN***";
}

getOriginalItem(groups: KotsConfigGroup[], itemName: string) {
for (let g = 0; g < groups.length; g++) {
const group = groups[g];
for (let i = 0; i < group.items.length; i++) {
const item = group.items[i];
if (item.name === itemName) {
return item;
}
}
}
return null;
}

private async getConfigDataFromFiles(files: FilesAsBuffers): Promise<ConfigData> {
let configSpec: string = "",
configValues: string = "",
Expand Down Expand Up @@ -185,27 +172,6 @@ export class KotsApp {
}
}

shouldUpdateConfigValues(configGroups: KotsConfigGroup[], configValues: any, item: KotsConfigItem): boolean {
if (item.hidden || item.when === "false" || (item.type === "password" && item.value === this.getPasswordMask())) {
return false;
}
if (item.name in configValues) {
return item.value !== configValues[item.name];
} else {
const originalItem = this.getOriginalItem(configGroups, item.name);
if (originalItem && item.value) {
if (originalItem.value) {
return item.value !== originalItem.value;
} else if (originalItem.default) {
return item.value !== originalItem.default;
} else {
return true;
}
}
}
return false;
}

async applyConfigValues(configSpec: string, configValues: string, license: string, registryInfo: KotsAppRegistryDetails): Promise<KotsConfigGroup[]> {
const templatedConfig = await kotsTemplateConfig(configSpec, configValues, license, registryInfo);

Expand All @@ -230,37 +196,6 @@ export class KotsApp {
}
}

async templateConfigGroups(stores: Stores, appId: string, sequence: string, configGroups: KotsConfigGroup[]): Promise<KotsConfigGroup[]> {
const app = await stores.kotsAppStore.getApp(appId);
const configData = await stores.kotsAppStore.getAppConfigData(appId, sequence);
const { configSpec, configValues } = configData!;

const parsedConfig = yaml.safeLoad(configSpec);
const parsedConfigValues = yaml.safeLoad(configValues);

const specConfigValues = parsedConfigValues.spec.values;
const specConfigGroups = parsedConfig.spec.groups;

configGroups.forEach(group => {
group.items.forEach(async item => {
if (this.shouldUpdateConfigValues(specConfigGroups, specConfigValues, item)) {
let configVal = {}
if (item.value) {
configVal["value"] = item.value;
}
if (item.default) {
configVal["default"] = item.default;
}
specConfigValues[item.name] = configVal;
}
});
});

const updatedConfigValues = yaml.safeDump(parsedConfigValues);
const registryInfo = await stores.kotsAppStore.getAppRegistryDetails(app.id);
return await this.applyConfigValues(configSpec, updatedConfigValues, String(app.license), registryInfo);
}

// Source files
async generateFileTreeIndex(sequence) {
const paths = await this.getFilesPaths(sequence);
Expand Down
7 changes: 0 additions & 7 deletions kotsadm/api/src/kots_app/resolvers/kots_app_queries.ts
Expand Up @@ -107,12 +107,5 @@ export function KotsQueries(stores: Stores, params: Params) {
const clusterId = await stores.clusterStore.getIdFromSlug(args.clusterSlug);
return await stores.kotsAppStore.getDownstreamOutput(app.id, clusterId, args.sequence);
},

async templateConfigGroups(root: any, args: any, context: Context): Promise<KotsConfigGroup[]> {
const { slug, sequence, configGroups } = args;
const appId = await stores.kotsAppStore.getIdFromSlug(slug)
const app = await context.getApp(appId);
return await app.templateConfigGroups(stores, app.id, sequence, configGroups);
},
};
}
1 change: 0 additions & 1 deletion kotsadm/api/src/schema/query.ts
Expand Up @@ -32,7 +32,6 @@ export const Query = `
getAppConfigGroups(slug: String!, sequence: Int!): [KotsConfigGroup]
getKotsDownstreamOutput(appSlug: String!, clusterSlug: String!, sequence: Int!): KotsDownstreamOutput
templateConfigGroups(slug: String!, sequence: Int!, configGroups: [KotsConfigGroupInput]!): [KotsConfigGroup]
listSupportBundles(watchSlug: String!): [SupportBundle]
getSupportBundle(watchSlug: String!): SupportBundle
Expand Down
1 change: 1 addition & 0 deletions kotsadm/pkg/apiserver/server.go
Expand Up @@ -133,6 +133,7 @@ func Start() {
r.Path("/api/v1/app/{appSlug}/registry").Methods("OPTIONS", "GET").HandlerFunc(handlers.GetAppRegistry)
r.Path("/api/v1/app/{appSlug}/registry/validate").Methods("OPTIONS", "POST").HandlerFunc(handlers.ValidateAppRegistry)
r.Path("/api/v1/app/{appSlug}/config").Methods("OPTIONS", "PUT").HandlerFunc(handlers.UpdateAppConfig)
r.Path("/api/v1/app/{appSlug}/liveconfig").Methods("OPTIONS", "POST").HandlerFunc(handlers.LiveAppConfig)
r.Path("/api/v1/app/{appSlug}/license").Methods("OPTIONS", "PUT").HandlerFunc(handlers.SyncLicense)
r.Path("/api/v1/app/{appSlug}/license").Methods("OPTIONS", "GET").HandlerFunc(handlers.GetLicense)
r.Path("/api/v1/app/{appSlug}/updatecheck").Methods("OPTIONS", "POST").HandlerFunc(handlers.AppUpdateCheck)
Expand Down
139 changes: 125 additions & 14 deletions kotsadm/pkg/handlers/config.go
Expand Up @@ -15,9 +15,10 @@ import (
"github.com/gorilla/mux"
"github.com/pkg/errors"
apptypes "github.com/replicatedhq/kots/kotsadm/pkg/app/types"
"github.com/replicatedhq/kots/kotsadm/pkg/config"
kotsadmconfig "github.com/replicatedhq/kots/kotsadm/pkg/config"
"github.com/replicatedhq/kots/kotsadm/pkg/downstream"
"github.com/replicatedhq/kots/kotsadm/pkg/kotsutil"
"github.com/replicatedhq/kots/kotsadm/pkg/license"
"github.com/replicatedhq/kots/kotsadm/pkg/logger"
"github.com/replicatedhq/kots/kotsadm/pkg/preflight"
"github.com/replicatedhq/kots/kotsadm/pkg/render"
Expand All @@ -27,7 +28,9 @@ import (
versiontypes "github.com/replicatedhq/kots/kotsadm/pkg/version/types"
kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
"github.com/replicatedhq/kots/kotskinds/multitype"
kotsconfig "github.com/replicatedhq/kots/pkg/config"
"github.com/replicatedhq/kots/pkg/crypto"
"github.com/replicatedhq/kots/pkg/template"
)

type UpdateAppConfigRequest struct {
Expand All @@ -36,12 +39,23 @@ type UpdateAppConfigRequest struct {
ConfigGroups []*kotsv1beta1.ConfigGroup `json:"configGroups"`
}

type LiveAppConfigRequest struct {
Sequence int64 `json:"sequence"`
ConfigGroups []kotsv1beta1.ConfigGroup `json:"configGroups"`
}

type UpdateAppConfigResponse struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
RequiredItems []string `json:"requiredItems,omitempty"`
}

type LiveAppConfigResponse struct {
Success bool `json:"success"`
Error string `json:"error,omitempty"`
ConfigGroups []kotsv1beta1.ConfigGroup `json:"configGroups"`
}

func UpdateAppConfig(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "content-type, origin, accept, authorization")
Expand All @@ -59,30 +73,30 @@ func UpdateAppConfig(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&updateAppConfigRequest); err != nil {
logger.Error(err)
updateAppConfigResponse.Error = "failed to decode request body"
JSON(w, 400, updateAppConfigResponse)
JSON(w, http.StatusBadRequest, updateAppConfigResponse)
return
}

sess, err := session.Parse(r.Header.Get("Authorization"))
if err != nil {
logger.Error(err)
updateAppConfigResponse.Error = "failed to parse authorization header"
JSON(w, 401, updateAppConfigResponse)
JSON(w, http.StatusUnauthorized, updateAppConfigResponse)
return
}

// we don't currently have roles, all valid tokens are valid sessions
if sess == nil || sess.ID == "" {
updateAppConfigResponse.Error = "failed to parse authorization header"
JSON(w, 401, updateAppConfigResponse)
JSON(w, http.StatusUnauthorized, updateAppConfigResponse)
return
}

foundApp, err := store.GetStore().GetAppFromSlug(mux.Vars(r)["appSlug"])
if err != nil {
logger.Error(err)
updateAppConfigResponse.Error = "failed to get app from app slug"
JSON(w, 500, updateAppConfigResponse)
JSON(w, http.StatusInternalServerError, updateAppConfigResponse)
return
}

Expand All @@ -92,16 +106,16 @@ func UpdateAppConfig(w http.ResponseWriter, r *http.Request) {
resp, err := updateAppConfig(foundApp, updateAppConfigRequest.Sequence, updateAppConfigRequest, true)
if err != nil {
logger.Error(err)
JSON(w, 500, resp)
JSON(w, http.StatusInternalServerError, resp)
return
}

if len(resp.RequiredItems) > 0 {
JSON(w, 400, resp)
JSON(w, http.StatusBadRequest, resp)
return
}

JSON(w, 200, resp)
JSON(w, http.StatusOK, resp)
return
}

Expand All @@ -113,20 +127,20 @@ func UpdateAppConfig(w http.ResponseWriter, r *http.Request) {
if err != nil {
logger.Error(err)
updateAppConfigResponse.Error = err.Error()
JSON(w, 500, updateAppConfigResponse)
JSON(w, http.StatusInternalServerError, updateAppConfigResponse)
return
}

// attempt to apply the config to the app version specified in the request
resp, err := updateAppConfig(foundApp, latestSequenceMatchingUpdateCursor, updateAppConfigRequest, true)
if err != nil {
logger.Error(err)
JSON(w, 500, resp)
JSON(w, http.StatusInternalServerError, resp)
return
}

if len(resp.RequiredItems) > 0 {
JSON(w, 400, resp)
JSON(w, http.StatusBadRequest, resp)
return
}

Expand All @@ -138,7 +152,104 @@ func UpdateAppConfig(w http.ResponseWriter, r *http.Request) {
}
}

JSON(w, 200, UpdateAppConfigResponse{Success: true})
JSON(w, http.StatusOK, UpdateAppConfigResponse{Success: true})
}

func LiveAppConfig(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "content-type, origin, accept, authorization")

if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}

liveAppConfigResponse := LiveAppConfigResponse{
Success: false,
}

liveAppConfigRequest := LiveAppConfigRequest{}
if err := json.NewDecoder(r.Body).Decode(&liveAppConfigRequest); err != nil {
logger.Error(err)
liveAppConfigResponse.Error = "failed to decode request body"
JSON(w, http.StatusBadRequest, liveAppConfigResponse)
return
}

sess, err := session.Parse(r.Header.Get("Authorization"))
if err != nil {
logger.Error(err)
liveAppConfigResponse.Error = "failed to parse authorization header"
JSON(w, http.StatusUnauthorized, liveAppConfigResponse)
return
}

// we don't currently have roles, all valid tokens are valid sessions
if sess == nil || sess.ID == "" {
liveAppConfigResponse.Error = "failed to parse authorization header"
JSON(w, http.StatusUnauthorized, liveAppConfigResponse)
return
}

foundApp, err := store.GetStore().GetAppFromSlug(mux.Vars(r)["appSlug"])
if err != nil {
logger.Error(err)
liveAppConfigResponse.Error = "failed to get app from app slug"
JSON(w, http.StatusInternalServerError, liveAppConfigResponse)
return
}

appLicense, err := license.Get(foundApp.ID)
if err != nil {
logger.Error(err)
liveAppConfigResponse.Error = "failed to get license for app"
JSON(w, http.StatusInternalServerError, liveAppConfigResponse)
return
}

archiveDir, err := version.GetAppVersionArchive(foundApp.ID, liveAppConfigRequest.Sequence)
if err != nil {
liveAppConfigResponse.Error = "failed to get app version archive"
JSON(w, http.StatusInternalServerError, liveAppConfigResponse)
return
}
defer os.RemoveAll(archiveDir)

kotsKinds, err := kotsutil.LoadKotsKindsFromPath(archiveDir)
if err != nil {
liveAppConfigResponse.Error = "failed to load kots kinds from path"
JSON(w, http.StatusInternalServerError, liveAppConfigResponse)
return
}

// get values from request
configValues := map[string]template.ItemValue{}

for _, group := range liveAppConfigRequest.ConfigGroups {
for _, item := range group.Items {
generatedValue := template.ItemValue{}
if item.Value.Type == multitype.String {
generatedValue.Value = item.Value.StrVal
} else {
generatedValue.Value = item.Value.BoolVal
}
if item.Default.Type == multitype.String {
generatedValue.Default = item.Default.StrVal
} else {
generatedValue.Default = item.Default.BoolVal
}
configValues[item.Name] = generatedValue
}
}

renderedConfig, err := kotsconfig.TemplateConfigObjects(kotsKinds.Config, configValues, appLicense, template.LocalRegistry{})
if err != nil {
liveAppConfigResponse.Error = "failed to render templates"
JSON(w, http.StatusInternalServerError, liveAppConfigResponse)
return
}

JSON(w, http.StatusOK, LiveAppConfigResponse{Success: true, ConfigGroups: renderedConfig.Spec.Groups})
}

// if isPrimaryVersion is false, missing a required config field will not cause a failure, and instead will create
Expand Down Expand Up @@ -169,7 +280,7 @@ func updateAppConfig(updateApp *apptypes.App, sequence int64, req UpdateAppConfi
continue
}
for _, item := range group.Items {
if config.IsRequiredItem(item) && config.IsUnsetItem(item) {
if kotsadmconfig.IsRequiredItem(item) && kotsadmconfig.IsUnsetItem(item) {
requiredItems = append(requiredItems, item.Name)
if item.Title != "" {
requiredItemsTitles = append(requiredItemsTitles, item.Title)
Expand Down Expand Up @@ -258,7 +369,7 @@ func updateAppConfig(updateApp *apptypes.App, sequence int64, req UpdateAppConfi
}
sequence = newSequence
} else {
if err := config.UpdateConfigValuesInDB(archiveDir, updateApp.ID, int64(sequence)); err != nil {
if err := kotsadmconfig.UpdateConfigValuesInDB(archiveDir, updateApp.ID, int64(sequence)); err != nil {
updateAppConfigResponse.Error = "failed to update config values in db"
return updateAppConfigResponse, err
}
Expand Down

0 comments on commit 448ccf1

Please sign in to comment.