Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract custom default values from package tarball #5702

Merged
merged 5 commits into from
Nov 30, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
24 changes: 16 additions & 8 deletions cmd/kubeapps-apis/docs/kubeapps-apis.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -4437,14 +4437,6 @@
],
"default": "PACKAGE_REPOSITORY_AUTH_TYPE_UNSPECIFIED"
},
"pluginsfluxv2packagesv1alpha1SetUserManagedSecretsResponse": {
"type": "object",
"properties": {
"value": {
"type": "boolean"
}
}
},
"protobufAny": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -4582,6 +4574,14 @@
"description": "An example of default values used during package templating that can serve\nas documentation or a starting point for user customization.",
"title": "Available package default values"
},
"customDefaultValues": {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is just a suggestion, but maybe we can use a more neutral word? The description is using the term extra for example, another term could be "additional" or "alternate"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd go for "additional" (as they are layered on top) rather than "alternate" (as they are not instead of), but yep, "additional" is good for me (better than "custom"). I'll update to that. Thanks.

"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "A package may contain extra default value files for specific scenarios,\nsuch as values_production.yaml or values_dev.yaml",
"title": "Available package custom default values"
},
"valuesSchema": {
"type": "string"
},
Expand Down Expand Up @@ -5565,6 +5565,14 @@
"description": "The type of secret. Currently Kubeapps itself only deals with OPAQUE\nand docker config json secrets, but we define all so we can correctly\nlist the secret names with their types.\nSee https://kubernetes.io/docs/concepts/configuration/secret/#secret-types",
"title": "SecretType"
},
"v1alpha1SetUserManagedSecretsResponse": {
"type": "object",
"properties": {
"value": {
"type": "boolean"
}
}
},
"v1alpha1SshCredentials": {
"type": "object",
"properties": {
Expand Down
903 changes: 465 additions & 438 deletions cmd/kubeapps-apis/gen/core/packages/v1alpha1/packages.pb.go

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,12 @@ message AvailablePackageDetail {
// as documentation or a starting point for user customization.
string default_values = 11;

// Available package custom default values
//
// A package may contain extra default value files for specific scenarios,
// such as values_production.yaml or values_dev.yaml
map<string, string> custom_default_values = 17;

// Available package values schema
//
// An optional openapi/json schema that can be used to validate a user-provided values.
Expand Down
1 change: 1 addition & 0 deletions dashboard/src/actions/availablepackages.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const defaultAvailablePackageDetail: AvailablePackageDetail = {
},
valuesSchema: "",
defaultValues: "",
customDefaultValues: {},
maintainers: [],
readme: "",
version: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const testProps: IPackageHeaderProps = {
},
valuesSchema: "",
defaultValues: "",
customDefaultValues: {},
maintainers: [],
readme: "",
version: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const defaultAvailablePkgDetail: AvailablePackageDetail = {
},
valuesSchema: "test",
defaultValues: "test",
customDefaultValues: {},
maintainers: [{ name: "test", email: "test" }] as Maintainer[],
readme: "test",
version: {
Expand Down
121 changes: 121 additions & 0 deletions dashboard/src/gen/kubeappsapis/core/packages/v1alpha1/packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,13 @@ export interface AvailablePackageDetail {
* as documentation or a starting point for user customization.
*/
defaultValues: string;
/**
* Available package custom default values
*
* A package may contain extra default value files for specific scenarios,
* such as values_production.yaml or values_dev.yaml
*/
customDefaultValues: { [key: string]: string };
valuesSchema: string;
/** source urls for the package */
sourceUrls: string[];
Expand Down Expand Up @@ -468,6 +475,11 @@ export interface AvailablePackageDetail {
customDetail?: Any;
}

export interface AvailablePackageDetail_CustomDefaultValuesEntry {
key: string;
value: string;
}

/**
* InstalledPackageSummary
*
Expand Down Expand Up @@ -2518,6 +2530,7 @@ function createBaseAvailablePackageDetail(): AvailablePackageDetail {
longDescription: "",
readme: "",
defaultValues: "",
customDefaultValues: {},
valuesSchema: "",
sourceUrls: [],
maintainers: [],
Expand Down Expand Up @@ -2564,6 +2577,12 @@ export const AvailablePackageDetail = {
if (message.defaultValues !== "") {
writer.uint32(90).string(message.defaultValues);
}
Object.entries(message.customDefaultValues).forEach(([key, value]) => {
AvailablePackageDetail_CustomDefaultValuesEntry.encode(
{ key: key as any, value },
writer.uint32(138).fork(),
).ldelim();
});
if (message.valuesSchema !== "") {
writer.uint32(98).string(message.valuesSchema);
}
Expand Down Expand Up @@ -2622,6 +2641,15 @@ export const AvailablePackageDetail = {
case 11:
message.defaultValues = reader.string();
break;
case 17:
const entry17 = AvailablePackageDetail_CustomDefaultValuesEntry.decode(
reader,
reader.uint32(),
);
if (entry17.value !== undefined) {
message.customDefaultValues[entry17.key] = entry17.value;
}
break;
case 12:
message.valuesSchema = reader.string();
break;
Expand Down Expand Up @@ -2660,6 +2688,15 @@ export const AvailablePackageDetail = {
longDescription: isSet(object.longDescription) ? String(object.longDescription) : "",
readme: isSet(object.readme) ? String(object.readme) : "",
defaultValues: isSet(object.defaultValues) ? String(object.defaultValues) : "",
customDefaultValues: isObject(object.customDefaultValues)
? Object.entries(object.customDefaultValues).reduce<{ [key: string]: string }>(
(acc, [key, value]) => {
acc[key] = String(value);
return acc;
},
{},
)
: {},
valuesSchema: isSet(object.valuesSchema) ? String(object.valuesSchema) : "",
sourceUrls: Array.isArray(object?.sourceUrls)
? object.sourceUrls.map((e: any) => String(e))
Expand Down Expand Up @@ -2691,6 +2728,12 @@ export const AvailablePackageDetail = {
message.longDescription !== undefined && (obj.longDescription = message.longDescription);
message.readme !== undefined && (obj.readme = message.readme);
message.defaultValues !== undefined && (obj.defaultValues = message.defaultValues);
obj.customDefaultValues = {};
if (message.customDefaultValues) {
Object.entries(message.customDefaultValues).forEach(([k, v]) => {
obj.customDefaultValues[k] = v;
});
}
message.valuesSchema !== undefined && (obj.valuesSchema = message.valuesSchema);
if (message.sourceUrls) {
obj.sourceUrls = message.sourceUrls.map(e => e);
Expand Down Expand Up @@ -2733,6 +2776,14 @@ export const AvailablePackageDetail = {
message.longDescription = object.longDescription ?? "";
message.readme = object.readme ?? "";
message.defaultValues = object.defaultValues ?? "";
message.customDefaultValues = Object.entries(object.customDefaultValues ?? {}).reduce<{
[key: string]: string;
}>((acc, [key, value]) => {
if (value !== undefined) {
acc[key] = String(value);
}
return acc;
}, {});
message.valuesSchema = object.valuesSchema ?? "";
message.sourceUrls = object.sourceUrls?.map(e => e) || [];
message.maintainers = object.maintainers?.map(e => Maintainer.fromPartial(e)) || [];
Expand All @@ -2745,6 +2796,72 @@ export const AvailablePackageDetail = {
},
};

function createBaseAvailablePackageDetail_CustomDefaultValuesEntry(): AvailablePackageDetail_CustomDefaultValuesEntry {
return { key: "", value: "" };
}

export const AvailablePackageDetail_CustomDefaultValuesEntry = {
encode(
message: AvailablePackageDetail_CustomDefaultValuesEntry,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.key !== "") {
writer.uint32(10).string(message.key);
}
if (message.value !== "") {
writer.uint32(18).string(message.value);
}
return writer;
},

decode(
input: _m0.Reader | Uint8Array,
length?: number,
): AvailablePackageDetail_CustomDefaultValuesEntry {
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAvailablePackageDetail_CustomDefaultValuesEntry();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
message.key = reader.string();
break;
case 2:
message.value = reader.string();
break;
default:
reader.skipType(tag & 7);
break;
}
}
return message;
},

fromJSON(object: any): AvailablePackageDetail_CustomDefaultValuesEntry {
return {
key: isSet(object.key) ? String(object.key) : "",
value: isSet(object.value) ? String(object.value) : "",
};
},

toJSON(message: AvailablePackageDetail_CustomDefaultValuesEntry): unknown {
const obj: any = {};
message.key !== undefined && (obj.key = message.key);
message.value !== undefined && (obj.value = message.value);
return obj;
},

fromPartial<I extends Exact<DeepPartial<AvailablePackageDetail_CustomDefaultValuesEntry>, I>>(
object: I,
): AvailablePackageDetail_CustomDefaultValuesEntry {
const message = createBaseAvailablePackageDetail_CustomDefaultValuesEntry();
message.key = object.key ?? "";
message.value = object.value ?? "";
return message;
},
};

function createBaseInstalledPackageSummary(): InstalledPackageSummary {
return {
installedPackageRef: undefined,
Expand Down Expand Up @@ -4368,6 +4485,10 @@ export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };

function isObject(value: any): boolean {
return typeof value === "object" && value !== null;
}

function isSet(value: any): boolean {
return value !== null && value !== undefined;
}
Expand Down
21 changes: 12 additions & 9 deletions pkg/chart/models/chart.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,16 @@ type ChartVersion struct {
Schema string `json:"schema" bson:"-"`
}

// ChartFiles holds the README and values for a given chart version
// ChartFiles holds the README and default values for a given chart version
type ChartFiles struct {
ID string `bson:"file_id"`
Readme string
Values string
Schema string
Repo *Repo
Digest string
// TODO(absoludity): Rename to DefaultValues in separate PR for easy review.
Values string
CustomDefaultValues map[string]string
Schema string
Repo *Repo
Digest string
}

// Allow to convert ChartFiles to a sql JSON
Expand All @@ -92,8 +94,9 @@ func (a ChartFiles) Value() (driver.Value, error) {

// some constant strings used as keys in maps in several modules
const (
ReadmeKey = "readme"
ValuesKey = "values"
SchemaKey = "schema"
ChartYamlKey = "chartYaml"
ReadmeKey = "readme"
ValuesKey = "values"
CustomDefaultValuesKey = "customDefaultValues"
SchemaKey = "schema"
ChartYamlKey = "chartYaml"
)
57 changes: 43 additions & 14 deletions pkg/tarutil/tarutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"io"
"net/url"
"path"
"regexp"
"strings"

chart "github.com/vmware-tanzu/kubeapps/pkg/chart/models"
Expand Down Expand Up @@ -80,20 +81,22 @@ func FetchChartDetailFromTarball(reader io.Reader, name string) (map[string]stri
chart.ChartYamlKey: chartYamlFileName,
}

files, err := ExtractFilesFromTarball(filenames, tarf)
if err != nil {
return nil, err
// Optionally search for files matching a regular expression, using the
// template to provide the key.
regexes := map[string]*regexp.Regexp{
chart.ValuesKey + "-$valuesType": regexp.MustCompile(fixedName + `/values-(?P<valuesType>\w+)\.yaml`),
}

return map[string]string{
chart.ValuesKey: files[chart.ValuesKey],
chart.ReadmeKey: files[chart.ReadmeKey],
chart.SchemaKey: files[chart.SchemaKey],
chart.ChartYamlKey: files[chart.ChartYamlKey],
}, nil
return ExtractFilesFromTarball(filenames, regexes, tarf)
}

func ExtractFilesFromTarball(filenames map[string]string, tarf *tar.Reader) (map[string]string, error) {
// ExtractFilesFromTarball returns the content of extracted files in a map.
//
// Files can be extracted by exact matches on the filename, or by regular
// expression matches. For exact matches, the key used in the resulting map
// is simply the key of the filename. For regex matches, a regexp template
// defines the key so that it can be expanded from the match.
func ExtractFilesFromTarball(filenames map[string]string, regexes map[string]*regexp.Regexp, tarf *tar.Reader) (map[string]string, error) {
ret := make(map[string]string)
for {
header, err := tarf.Next()
Expand All @@ -104,17 +107,43 @@ func ExtractFilesFromTarball(filenames map[string]string, tarf *tar.Reader) (map
return ret, err
}

foundFile := false
for id, f := range filenames {
if strings.EqualFold(header.Name, f) {
var b bytes.Buffer
_, err := io.Copy(&b, tarf)
if err != nil {
if s, err := readTarFileContent(tarf); err != nil {
return ret, err
} else {
ret[id] = s
}
ret[id] = b.String()
foundFile = true
break
}
}
if foundFile {
continue
}

for template, pattern := range regexes {
match := pattern.FindSubmatchIndex([]byte(header.Name))
if match != nil {
result := []byte{}
result = pattern.ExpandString(result, template, header.Name, match)
if s, err := readTarFileContent(tarf); err != nil {
return ret, err
} else {
ret[string(result)] = s
}
}
}
}
return ret, nil
}

func readTarFileContent(tarf *tar.Reader) (string, error) {
var b bytes.Buffer
_, err := io.Copy(&b, tarf)
if err != nil {
return "", err
}
return b.String(), nil
}
Loading