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

Add recipe support for Kubernetes secrets / OpenShift routes #12486

Merged
merged 4 commits into from
Feb 1, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* Red Hat, Inc. - initial API and implementation
*/
'use strict';
import {ISupportedListItem, KubernetesMachineRecipeParser} from './kubernetes-machine-recipe-parser';
import {ISupportedListItem, KubernetesMachineRecipeParser, isSupportedItem} from './kubernetes-machine-recipe-parser';
import {IParser} from './parser';

export interface ISupportedItemList {
Expand Down Expand Up @@ -79,14 +79,20 @@ export class KubernetesEnvironmentRecipeParser implements IParser {
if (!angular.isArray(items) || items.length === 0) {
throw new TypeError(`Recipe kubernetes list should contain at least one 'item'.`);
} else {
items.forEach((item: ISupportedListItem) => {
items.forEach((item: any) => {
if (!item) {
return;
}
// skip services
if (item.kind && item.kind.toLowerCase() === 'service') {
return;
}
if (!isSupportedItem(item)) {
// should throw a TypeError here but this code is currently used to validate OpenShift recipes
// (which support Routes) as well as Kubernetes recipes, so we need to ignore some elements
// rather than complain. Returning here prevents warning about typos in the `kind` section.
return;
}
this.machineRecipeParser.validate(item);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@

import {IParser} from './parser';

export type ISupportedListItem = IPodItem | IDeploymentItem | IConfigMapItem;
export type ISupportedListItem = IPodItem | IDeploymentItem | IConfigMapItem | ISecretItem;

export function isSupportedItem(item: any): item is ISupportedListItem {
return isDeploymentItem(item) || isPodItem(item) || isConfigMapItem(item) || isSecretItem(item);
}

export function isDeploymentItem(item: ISupportedListItem): item is IDeploymentItem {
return (item.kind && item.kind.toLowerCase() === 'deployment');
Expand All @@ -27,6 +31,10 @@ export function isConfigMapItem(item: ISupportedListItem): item is IConfigMapIte
return (item.kind && item.kind.toLowerCase() === 'configmap');
}

export function isSecretItem(item: ISupportedListItem): item is ISecretItem {
return (item.kind && item.kind.toLowerCase() === 'secret');
}

export function getPodItemOrNull(item: ISupportedListItem): IPodItem {
if (isDeploymentItem(item)) {
return item.spec.template;
Expand Down Expand Up @@ -84,6 +92,14 @@ export interface IConfigMapItem {
data: { [propName: string]: string | Object };
}

export interface ISecretItem {
apiVersion: string;
kind: string;
metadata: IObjectMetadata;
data?: { [propName: string]: string | Object };
stringData?: { [propName: string]: string | Object};
}

/**
* Wrapper for jsyaml and simple validator for kubernetes machine recipe.
*
Expand Down Expand Up @@ -141,6 +157,8 @@ export class KubernetesMachineRecipeParser implements IParser {
this.validatePod(<IPodItem>recipe);
} else if (isConfigMapItem(recipe)) {
this.validateConfigMap(<IConfigMapItem>recipe);
} else if (isSecretItem(recipe)) {
this.validateSecret(<ISecretItem> recipe);
}
}

Expand Down Expand Up @@ -224,6 +242,15 @@ export class KubernetesMachineRecipeParser implements IParser {
}
}

validateSecret(secret: ISecretItem) {
this.validateMetadata(secret.metadata);
if (!secret.data && !secret.stringData) {
throw new TypeError(`Recipe secret item should contain either data or stringData section.`);
}
// secret.data values must also be base64 encoded but nodejs doesn't allow an easy way to check
// if the encoding is valid (ignores errors silently).
}

validateMetadata(metadata: IObjectMetadata) {
if (!metadata) {
throw new TypeError(`Recipe item should contain 'metadata' section.`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@ public final class Warnings {
"Ingresses specified in Kubernetes recipe are ignored. "
+ "To expose ports please define servers in machine configuration.";

public static final int SECRET_IGNORED_WARNING_CODE = 4102;
public static final String SECRET_IGNORED_WARNING_MESSAGE =
"Secrets specified in Kubernetes recipe are ignored.";

public static final int RESTART_POLICY_SET_TO_NEVER_WARNING_CODE = 4104;
public static final String RESTART_POLICY_SET_TO_NEVER_WARNING_MESSAGE_FMT =
"Restart policy '%s' for pod '%s' is rewritten with %s";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.extensions.Ingress;
import io.fabric8.kubernetes.client.KubernetesClientException;
import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.Collection;
Expand Down Expand Up @@ -97,16 +98,28 @@ protected KubernetesEnvironment doCreate(
+ "application/x-yaml, text/yaml, text/x-yaml");
}

final KubernetesList list =
clientFactory.create().lists().load(new ByteArrayInputStream(content.getBytes())).get();
final KubernetesList list;
try {
list =
clientFactory.create().lists().load(new ByteArrayInputStream(content.getBytes())).get();
} catch (KubernetesClientException e) {
// KubernetesClient wraps the error when a JsonMappingException occurs so we need the cause
String message = e.getCause() == null ? e.getMessage() : e.getCause().getMessage();
if (message.contains("\n")) {
// Clean up message if it comes from JsonMappingException. Format is e.g.
// `No resource type found for:v1#Route1\n at [...]`
message = message.split("\\n", 2)[0];
}
throw new ValidationException(format("Could not parse Kubernetes recipe: %s", message));
}

Map<String, Pod> pods = new HashMap<>();
Map<String, Deployment> deployments = new HashMap<>();
Map<String, Service> services = new HashMap<>();
Map<String, ConfigMap> configMaps = new HashMap<>();
Map<String, PersistentVolumeClaim> pvcs = new HashMap<>();
Map<String, Secret> secrets = new HashMap<>();
boolean isAnyIngressPresent = false;
boolean isAnySecretPresent = false;
for (HasMetadata object : list.getItems()) {
checkNotNull(object.getKind(), "Environment contains object without specified kind field");
checkNotNull(object.getMetadata(), "%s metadata must not be null", object.getKind());
Expand All @@ -127,13 +140,16 @@ protected KubernetesEnvironment doCreate(
PersistentVolumeClaim pvc = (PersistentVolumeClaim) object;
pvcs.put(pvc.getMetadata().getName(), pvc);
} else if (object instanceof Secret) {
isAnySecretPresent = true;
Secret secret = (Secret) object;
amisevsk marked this conversation as resolved.
Show resolved Hide resolved
secrets.put(secret.getMetadata().getName(), secret);
} else if (object instanceof ConfigMap) {
ConfigMap configMap = (ConfigMap) object;
configMaps.put(configMap.getMetadata().getName(), configMap);
} else {
throw new ValidationException(
format("Found unknown object type '%s'", object.getMetadata()));
format(
"Found unknown object type in recipe -- name: '%s', kind: '%s'",
object.getMetadata().getName(), object.getKind()));
}
}

Expand All @@ -143,12 +159,6 @@ protected KubernetesEnvironment doCreate(
Warnings.INGRESSES_IGNORED_WARNING_CODE, Warnings.INGRESSES_IGNORED_WARNING_MESSAGE));
}

if (isAnySecretPresent) {
warnings.add(
new WarningImpl(
Warnings.SECRET_IGNORED_WARNING_CODE, Warnings.SECRET_IGNORED_WARNING_MESSAGE));
}

addRamAttributes(machines, pods.values());

KubernetesEnvironment k8sEnv =
Expand All @@ -162,7 +172,7 @@ protected KubernetesEnvironment doCreate(
.setPersistentVolumeClaims(pvcs)
.setIngresses(new HashMap<>())
.setPersistentVolumeClaims(new HashMap<>())
.setSecrets(new HashMap<>())
.setSecrets(secrets)
.setConfigMaps(configMaps)
.build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.MACHINE_NAME_ANNOTATION_FMT;
import static org.eclipse.che.workspace.infrastructure.kubernetes.Warnings.INGRESSES_IGNORED_WARNING_CODE;
import static org.eclipse.che.workspace.infrastructure.kubernetes.Warnings.INGRESSES_IGNORED_WARNING_MESSAGE;
import static org.eclipse.che.workspace.infrastructure.kubernetes.Warnings.SECRET_IGNORED_WARNING_CODE;
import static org.eclipse.che.workspace.infrastructure.kubernetes.Warnings.SECRET_IGNORED_WARNING_MESSAGE;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
Expand Down Expand Up @@ -50,6 +48,7 @@
import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder;
import io.fabric8.kubernetes.api.model.Quantity;
import io.fabric8.kubernetes.api.model.ResourceRequirements;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.ServiceBuilder;
Expand Down Expand Up @@ -178,20 +177,19 @@ public void ignoreIgressesWhenRecipeContainsThem() throws Exception {
}

@Test
public void ignoreSecretsWhenRecipeContainsThem() throws Exception {
final List<HasMetadata> recipeObjects =
singletonList(
new SecretBuilder().withNewMetadata().withName("secret").endMetadata().build());
public void addSecretsWhenRecipeContainsThem() throws Exception {
Secret secret =
new SecretBuilder().withNewMetadata().withName("test-secret").endMetadata().build();
final List<HasMetadata> recipeObjects = singletonList(secret);
when(parsedList.getItems()).thenReturn(recipeObjects);

final KubernetesEnvironment parsed =
k8sEnvFactory.doCreate(internalRecipe, emptyMap(), emptyList());

assertTrue(parsed.getSecrets().isEmpty());
assertEquals(parsed.getWarnings().size(), 1);
assertEquals(parsed.getSecrets().size(), 1);
assertEquals(
parsed.getWarnings().get(0),
new WarningImpl(SECRET_IGNORED_WARNING_CODE, SECRET_IGNORED_WARNING_MESSAGE));
parsed.getSecrets().get("test-secret").getMetadata().getName(),
secret.getMetadata().getName());
}

@Test
Expand Down

This file was deleted.

Loading