Skip to content

Commit

Permalink
1164 bug fix configmaps for jobs recreated every sync (#1168)
Browse files Browse the repository at this point in the history
* refactor use of constants in tests

* fix bug causing configmaps to be deleted and recreated
add missing tests for configmaps

* update chart versions
  • Loading branch information
nilsgstrabo authored Aug 19, 2024
1 parent 555d4e9 commit f217ad6
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 94 deletions.
4 changes: 2 additions & 2 deletions charts/radix-operator/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: v2
name: radix-operator
version: 1.37.4
appVersion: 1.57.12
version: 1.37.5
appVersion: 1.57.13
kubeVersion: ">=1.24.0"
description: Radix Operator
keywords:
Expand Down
10 changes: 4 additions & 6 deletions pkg/apis/deployment/config_maps.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,12 @@ func (deploy *Deployment) garbageCollectConfigMapsNoLongerInSpec(ctx context.Con
return fmt.Errorf("could not determine component name from labels in config map %s", cm.Name)
}

if !componentName.ExistInDeploymentSpecComponentList(deploy.radixDeployment) {
if !componentName.ExistInDeploymentSpec(deploy.radixDeployment) {
log.Ctx(ctx).Debug().Msgf("ConfigMap object %s in namespace %s belongs to deleted component %s, garbage collecting the configmap", cm.Name, namespace, componentName)
err = deploy.kubeutil.DeleteConfigMap(ctx, namespace, cm.Name)
if err = deploy.kubeutil.DeleteConfigMap(ctx, namespace, cm.Name); err != nil {
errs = append(errs, err)
}
}
if err != nil {
errs = append(errs, err)
}

}
return errors.Join(errs...)
}
193 changes: 153 additions & 40 deletions pkg/apis/deployment/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ import (
secretproviderfake "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned/fake"
)

const testClusterName = "AnyClusterName"
const dnsZone = "dev.radix.equinor.com"
const anyContainerRegistry = "any.container.registry"
const testEgressIps = "0.0.0.0"
const (
testClusterName = "AnyClusterName"
testEgressIps = "0.0.0.0"
)

var testConfig = config.Config{
DeploymentSyncer: deployment.SyncerConfig{
Expand Down Expand Up @@ -254,8 +254,8 @@ func TestObjectSynced_MultiComponent_ContainsAllElements(t *testing.T) {
assert.Equal(t, int32(1), pdbs.Items[0].Spec.MinAvailable.IntVal)

assert.Equal(t, 13, len(getContainerByName(componentNameApp, getDeploymentByName(componentNameApp, deployments).Spec.Template.Spec.Containers).Env), "number of environment variables was unexpected for component. It should contain default and custom")
assert.Equal(t, anyContainerRegistry, getEnvVariableByNameOnDeployment(kubeclient, defaults.ContainerRegistryEnvironmentVariable, componentNameApp, deployments))
assert.Equal(t, dnsZone, getEnvVariableByNameOnDeployment(kubeclient, defaults.RadixDNSZoneEnvironmentVariable, componentNameApp, deployments))
assert.Equal(t, os.Getenv(defaults.ContainerRegistryEnvironmentVariable), getEnvVariableByNameOnDeployment(kubeclient, defaults.ContainerRegistryEnvironmentVariable, componentNameApp, deployments))
assert.Equal(t, os.Getenv(defaults.OperatorDNSZoneEnvironmentVariable), getEnvVariableByNameOnDeployment(kubeclient, defaults.RadixDNSZoneEnvironmentVariable, componentNameApp, deployments))
assert.Equal(t, "AnyClusterName", getEnvVariableByNameOnDeployment(kubeclient, defaults.ClusternameEnvironmentVariable, componentNameApp, deployments))
assert.Equal(t, environment, getEnvVariableByNameOnDeployment(kubeclient, defaults.EnvironmentnameEnvironmentVariable, componentNameApp, deployments))
assert.Equal(t, "app-edcradix-test.AnyClusterName.dev.radix.equinor.com", getEnvVariableByNameOnDeployment(kubeclient, defaults.PublicEndpointEnvironmentVariable, componentNameApp, deployments))
Expand Down Expand Up @@ -644,8 +644,8 @@ func TestObjectSynced_MultiJob_ContainsAllElements(t *testing.T) {
envVars := getContainerByName(jobName, getDeploymentByName(jobName, deployments).Spec.Template.Spec.Containers).Env
assert.Equal(t, 14, len(envVars), "number of environment variables was unexpected for component. It should contain default and custom")
assert.Equal(t, "a_value", getEnvVariableByNameOnDeployment(kubeclient, "a_variable", jobName, deployments))
assert.Equal(t, anyContainerRegistry, getEnvVariableByNameOnDeployment(kubeclient, defaults.ContainerRegistryEnvironmentVariable, jobName, deployments))
assert.Equal(t, dnsZone, getEnvVariableByNameOnDeployment(kubeclient, defaults.RadixDNSZoneEnvironmentVariable, jobName, deployments))
assert.Equal(t, os.Getenv(defaults.ContainerRegistryEnvironmentVariable), getEnvVariableByNameOnDeployment(kubeclient, defaults.ContainerRegistryEnvironmentVariable, jobName, deployments))
assert.Equal(t, os.Getenv(defaults.OperatorDNSZoneEnvironmentVariable), getEnvVariableByNameOnDeployment(kubeclient, defaults.RadixDNSZoneEnvironmentVariable, jobName, deployments))
assert.Equal(t, "AnyClusterName", getEnvVariableByNameOnDeployment(kubeclient, defaults.ClusternameEnvironmentVariable, jobName, deployments))
assert.Equal(t, environment, getEnvVariableByNameOnDeployment(kubeclient, defaults.EnvironmentnameEnvironmentVariable, jobName, deployments))
assert.Equal(t, appName, getEnvVariableByNameOnDeployment(kubeclient, defaults.RadixAppEnvironmentVariable, jobName, deployments))
Expand Down Expand Up @@ -1296,66 +1296,178 @@ func TestConfigMap_IsGarbageCollected(t *testing.T) {
// Setup
tu, client, kubeUtil, radixclient, kedaClient, prometheusclient, _, certClient := SetupTest(t)
defer TeardownTest()
appName := "app"
comp1, comp2, job1, job2 := "comp1", "comp2", "job1", "job2"
anyEnvironment := "test"
namespace := utils.GetEnvironmentNamespace(appName, anyEnvironment)

// Test
_, err := ApplyDeploymentWithSync(tu, client, kubeUtil, radixclient, kedaClient, prometheusclient, certClient, utils.ARadixDeployment().
WithAppName(appName).
WithEnvironment(anyEnvironment).
WithJobComponents().
WithComponents(
utils.NewDeployComponentBuilder().
WithName("somecomponentname").
WithEnvironmentVariables(nil).
WithSecrets(nil),
utils.NewDeployComponentBuilder().
WithName(componentName).
WithEnvironmentVariables(nil).
WithSecrets(nil)),
utils.NewDeployComponentBuilder().WithName(comp1),
utils.NewDeployComponentBuilder().WithName(comp2),
).
WithJobComponents(
utils.NewDeployJobComponentBuilder().WithName(job1),
utils.NewDeployJobComponentBuilder().WithName(job2),
),
)
require.NoError(t, err)

cmNameMapper := func(cm *corev1.ConfigMap) string { return cm.Name }
componentsEnvNames := func(names ...string) []string {
return slice.Map(names, func(n string) string { return kube.GetEnvVarsConfigMapName(n) })
}
componentsEnvMetaNames := func(names ...string) []string {
return slice.Map(names, func(n string) string { return kube.GetEnvVarsMetadataConfigMapName(n) })
}

// check that config maps with env vars and env vars metadata were created
envVarCm, err := kubeUtil.GetConfigMap(context.Background(), namespace, kube.GetEnvVarsConfigMapName(componentName))
assert.NoError(t, err)
envVarMetadataCm, err := kubeUtil.GetConfigMap(context.Background(), namespace, kube.GetEnvVarsMetadataConfigMapName(componentName))
assert.NoError(t, err)
assert.NotNil(t, envVarCm)
assert.NotNil(t, envVarMetadataCm)
envVarCms, err := kubeUtil.ListEnvVarsConfigMaps(context.Background(), namespace)
assert.NoError(t, err)
assert.Len(t, envVarCms, 2)
assert.ElementsMatch(t, componentsEnvNames(comp1, comp2, job1, job2), slice.Map(envVarCms, cmNameMapper))
envVarMetadataCms, err := kubeUtil.ListEnvVarsMetadataConfigMaps(context.Background(), namespace)
assert.NoError(t, err)
assert.Len(t, envVarMetadataCms, 2)
assert.ElementsMatch(t, componentsEnvMetaNames(comp1, comp2, job1, job2), slice.Map(envVarMetadataCms, cmNameMapper))

// delete 2nd component
_, err = ApplyDeploymentWithSync(tu, client, kubeUtil, radixclient, kedaClient, prometheusclient, certClient, utils.ARadixDeployment().
WithAppName(appName).
WithEnvironment(anyEnvironment).
WithJobComponents().
WithComponents(
utils.NewDeployComponentBuilder().
WithName("somecomponentname").
WithEnvironmentVariables(nil).
WithSecrets(nil)),
utils.NewDeployComponentBuilder().WithName(comp1),
).
WithJobComponents(
utils.NewDeployJobComponentBuilder().WithName(job1),
),
)
require.NoError(t, err)

// check that config maps were garbage collected for the component we just deleted
envVarCm, err = kubeUtil.GetConfigMap(context.Background(), namespace, kube.GetEnvVarsConfigMapName(componentName))
assert.Error(t, err)
envVarMetadataCm, err = kubeUtil.GetConfigMap(context.Background(), namespace, kube.GetEnvVarsMetadataConfigMapName(componentName))
assert.Error(t, err)
assert.Nil(t, envVarCm)
assert.Nil(t, envVarMetadataCm)
// check that config maps were garbage collected for the component and job we just deleted
envVarCms, err = kubeUtil.ListEnvVarsConfigMaps(context.Background(), namespace)
assert.NoError(t, err)
assert.Len(t, envVarCms, 1)
assert.ElementsMatch(t, componentsEnvNames(comp1, job1), slice.Map(envVarCms, cmNameMapper))
envVarMetadataCms, err = kubeUtil.ListEnvVarsMetadataConfigMaps(context.Background(), namespace)
assert.NoError(t, err)
assert.Len(t, envVarMetadataCms, 1)
assert.ElementsMatch(t, componentsEnvMetaNames(comp1, job1), slice.Map(envVarMetadataCms, cmNameMapper))
}

func TestConfigMap_RetainDataBetweenSync(t *testing.T) {
// Setup
tu, client, kubeUtil, radixclient, kedaClient, prometheusclient, _, certClient := SetupTest(t)
defer TeardownTest()
appName := "app"
comp, job1, job2 := "comp", "job1", "job2"
anyEnvironment := "test"
namespace := utils.GetEnvironmentNamespace(appName, anyEnvironment)

// Initial apply of RadixDeployment
_, err := ApplyDeploymentWithSync(tu, client, kubeUtil, radixclient, kedaClient, prometheusclient, certClient, utils.ARadixDeployment().
WithAppName(appName).
WithEnvironment(anyEnvironment).
WithComponents(
utils.NewDeployComponentBuilder().
WithName(comp).
WithEnvironmentVariables(map[string]string{"COMPVAR1": "comp_original1", "COMPVAR2": "comp_original2"}),
).
WithJobComponents(
utils.NewDeployJobComponentBuilder().
WithName(job1).
WithEnvironmentVariables(map[string]string{"JOB1VAR1": "job1_original1", "JOB1VAR2": "job1_original2"}),
utils.NewDeployJobComponentBuilder().
WithName(job2).
WithEnvironmentVariables(map[string]string{"JOB2VAR1": "job2_original1", "JOB2VAR2": "job2_original2"}),
),
)
require.NoError(t, err)

// Check initial state of configmaps
// comp configmaps
varCm, _, varMeta, err := kubeUtil.GetEnvVarsConfigMapAndMetadataMap(context.Background(), namespace, comp)
require.NoError(t, err)
expectedData := map[string]string{"COMPVAR1": "comp_original1", "COMPVAR2": "comp_original2"}
assert.Equal(t, expectedData, varCm.Data)
assert.Empty(t, varMeta)
// job1 configmaps
varCm, _, varMeta, err = kubeUtil.GetEnvVarsConfigMapAndMetadataMap(context.Background(), namespace, job1)
require.NoError(t, err)
expectedData = map[string]string{"JOB1VAR1": "job1_original1", "JOB1VAR2": "job1_original2"}
assert.Equal(t, expectedData, varCm.Data)
assert.Empty(t, varMeta)
// job2 configmaps
varCm, _, varMeta, err = kubeUtil.GetEnvVarsConfigMapAndMetadataMap(context.Background(), namespace, job2)
require.NoError(t, err)
expectedData = map[string]string{"JOB2VAR1": "job2_original1", "JOB2VAR2": "job2_original2"}
assert.Equal(t, expectedData, varCm.Data)
assert.Empty(t, varMeta)

// Update variables and metadata configmaps, the way radix-api will do a change from a user
updateVariable := func(compName, varName, newVal, configValue string) error {
varCm, varMetaCm, varMeta, err := kubeUtil.GetEnvVarsConfigMapAndMetadataMap(context.Background(), namespace, compName)
if err != nil {
return err
}
updatedVarCm := varCm.DeepCopy()
updatedVarCm.Data[varName] = newVal
varMeta[varName] = kube.EnvVarMetadata{RadixConfigValue: configValue}
err = kubeUtil.ApplyConfigMap(context.Background(), namespace, varCm, updatedVarCm)
if err != nil {
return err
}
return kubeUtil.ApplyEnvVarsMetadataConfigMap(context.Background(), namespace, varMetaCm, varMeta)
}
// Change comp configmaps
err = updateVariable(comp, "COMPVAR1", "comp_new1", "comp_original1")
require.NoError(t, err)
// Change job1 configmaps
err = updateVariable(job1, "JOB1VAR1", "job1_new1", "job1_original1")
require.NoError(t, err)
// Change job2 configmaps
err = updateVariable(job2, "JOB2VAR1", "job2_new1", "job2_original1")
require.NoError(t, err)

// Apply update of RadixDeployment - job2 is moved from jobs to components
_, err = ApplyDeploymentWithSync(tu, client, kubeUtil, radixclient, kedaClient, prometheusclient, certClient, utils.ARadixDeployment().
WithAppName(appName).
WithEnvironment(anyEnvironment).
WithComponents(
utils.NewDeployComponentBuilder().
WithName(comp).
WithEnvironmentVariables(map[string]string{"COMPVAR1": "comp_original1", "COMPVAR2": "comp_original2"}),
utils.NewDeployComponentBuilder().
WithName(job2).
WithEnvironmentVariables(map[string]string{"JOB2VAR1": "job2_original1", "JOB2VAR2": "job2_original2"}),
).
WithJobComponents(
utils.NewDeployJobComponentBuilder().
WithName(job1).
WithEnvironmentVariables(map[string]string{"JOB1VAR1": "job1_original1", "JOB1VAR2": "job1_original2"}),
),
)
require.NoError(t, err)

// // Check state of configmaps is kept between syncs
// // comp configmaps
varCm, _, varMeta, err = kubeUtil.GetEnvVarsConfigMapAndMetadataMap(context.Background(), namespace, comp)
require.NoError(t, err)
expectedData = map[string]string{"COMPVAR1": "comp_new1", "COMPVAR2": "comp_original2"}
assert.Equal(t, expectedData, varCm.Data)
assert.Equal(t, map[string]kube.EnvVarMetadata{"COMPVAR1": {RadixConfigValue: "comp_original1"}}, varMeta)
// job1 configmaps
varCm, _, varMeta, err = kubeUtil.GetEnvVarsConfigMapAndMetadataMap(context.Background(), namespace, job1)
require.NoError(t, err)
expectedData = map[string]string{"JOB1VAR1": "job1_new1", "JOB1VAR2": "job1_original2"}
assert.Equal(t, expectedData, varCm.Data)
assert.Equal(t, map[string]kube.EnvVarMetadata{"JOB1VAR1": {RadixConfigValue: "job1_original1"}}, varMeta)
// job2 configmaps
varCm, _, varMeta, err = kubeUtil.GetEnvVarsConfigMapAndMetadataMap(context.Background(), namespace, job2)
require.NoError(t, err)
expectedData = map[string]string{"JOB2VAR1": "job2_new1", "JOB2VAR2": "job2_original2"}
assert.Equal(t, expectedData, varCm.Data)
assert.Equal(t, map[string]kube.EnvVarMetadata{"JOB2VAR1": {RadixConfigValue: "job2_original1"}}, varMeta)
}

func TestObjectSynced_NoEnvAndNoSecrets_ContainsDefaultEnvVariables(t *testing.T) {
Expand Down Expand Up @@ -1396,8 +1508,8 @@ func TestObjectSynced_NoEnvAndNoSecrets_ContainsDefaultEnvVariables(t *testing.T
assert.True(t, envVariableByNameExist(defaults.RadixCommitHashEnvironmentVariable, templateSpecEnv))
assert.True(t, envVariableByNameExist(defaults.RadixActiveClusterEgressIpsEnvironmentVariable, templateSpecEnv))
assert.True(t, envVariableByNameExist(defaults.RadixCommitHashEnvironmentVariable, templateSpecEnv))
assert.Equal(t, anyContainerRegistry, getEnvVariableByName(defaults.ContainerRegistryEnvironmentVariable, templateSpecEnv, nil))
assert.Equal(t, dnsZone, getEnvVariableByName(defaults.RadixDNSZoneEnvironmentVariable, templateSpecEnv, cm))
assert.Equal(t, os.Getenv(defaults.ContainerRegistryEnvironmentVariable), getEnvVariableByName(defaults.ContainerRegistryEnvironmentVariable, templateSpecEnv, nil))
assert.Equal(t, os.Getenv(defaults.OperatorDNSZoneEnvironmentVariable), getEnvVariableByName(defaults.RadixDNSZoneEnvironmentVariable, templateSpecEnv, cm))
assert.Equal(t, testClusterName, getEnvVariableByName(defaults.ClusternameEnvironmentVariable, templateSpecEnv, cm))
assert.Equal(t, anyEnvironment, getEnvVariableByName(defaults.EnvironmentnameEnvironmentVariable, templateSpecEnv, cm))
assert.Equal(t, "app", getEnvVariableByName(defaults.RadixAppEnvironmentVariable, templateSpecEnv, cm))
Expand Down Expand Up @@ -3846,6 +3958,7 @@ func TestRadixBatch_IsGarbageCollected(t *testing.T) {
// Setup
tu, client, kubeUtil, radixclient, kedaClient, prometheusclient, _, certClient := SetupTest(t)
defer TeardownTest()
appName := "app"
anyEnvironment := "test"
namespace := utils.GetEnvironmentNamespace(appName, anyEnvironment)

Expand Down
14 changes: 7 additions & 7 deletions pkg/apis/deployment/environmentvariables_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func Test_GetEnvironmentVariables(t *testing.T) {

assert.NoError(t, err)
assert.Len(t, envVars, 3)
envVarsConfigMap, envVarsConfigMapMetadata, err := testEnv.kubeUtil.GetOrCreateEnvVarsConfigMapAndMetadataMap(context.Background(), utils.GetEnvironmentNamespace(appName, env), appName, componentName)
envVarsConfigMap, envVarsConfigMapMetadata, err := testEnv.kubeUtil.GetOrCreateEnvVarsConfigMapAndMetadataMap(context.Background(), utils.GetEnvironmentNamespace(appName, envName), appName, componentName)
assert.NoError(t, err)
assert.NotNil(t, envVarsConfigMap)
assert.NotNil(t, envVarsConfigMap.Data)
Expand Down Expand Up @@ -111,7 +111,7 @@ func Test_getEnvironmentVariablesForRadixOperator(t *testing.T) {

assert.NoError(t, err)
assert.True(t, len(envVars) > 3)
envVarsConfigMap, envVarsConfigMapMetadata, err := testEnv.kubeUtil.GetOrCreateEnvVarsConfigMapAndMetadataMap(context.Background(), utils.GetEnvironmentNamespace(appName, env), appName, componentName)
envVarsConfigMap, envVarsConfigMapMetadata, err := testEnv.kubeUtil.GetOrCreateEnvVarsConfigMapAndMetadataMap(context.Background(), utils.GetEnvironmentNamespace(appName, envName), appName, componentName)
assert.NoError(t, err)
assert.NotNil(t, envVarsConfigMap)
assert.NotNil(t, envVarsConfigMap.Data)
Expand All @@ -131,7 +131,7 @@ func Test_getEnvironmentVariablesForRadixOperator(t *testing.T) {
func Test_RemoveFromConfigMapEnvVarsNotExistingInRadixDeployment(t *testing.T) {
appName := "any-app"
envName := "dev"
namespace := utils.GetEnvironmentNamespace(appName, env)
namespace := utils.GetEnvironmentNamespace(appName, envName)
componentName := "any-component"
testEnv := setupTestEnv(t)
defer TeardownTest()
Expand Down Expand Up @@ -168,12 +168,12 @@ func Test_RemoveFromConfigMapEnvVarsNotExistingInRadixDeployment(t *testing.T) {

assert.NoError(t, err)
assert.Len(t, envVars, 3)
envVarsConfigMap, envVarsMetadataConfigMap, err := testEnv.kubeUtil.GetOrCreateEnvVarsConfigMapAndMetadataMap(context.Background(), utils.GetEnvironmentNamespace(appName, env), appName, componentName)
envVarsConfigMap, envVarsMetadataConfigMap, err := testEnv.kubeUtil.GetOrCreateEnvVarsConfigMapAndMetadataMap(context.Background(), utils.GetEnvironmentNamespace(appName, envName), appName, componentName)
assert.NoError(t, err)
assert.NotNil(t, envVarsConfigMap)
assert.NotNil(t, envVarsConfigMap.Data)
assert.Len(t, envVarsConfigMap.Data, 3)
assert.Equal(t, "new-val1", envVarsConfigMap.Data["VAR1"])
assert.Equal(t, "val1", envVarsConfigMap.Data["VAR1"])
assert.Equal(t, "val2", envVarsConfigMap.Data["VAR2"])
assert.Equal(t, "val3", envVarsConfigMap.Data["VAR3"])
assert.Equal(t, "", envVarsConfigMap.Data["OUTDATED_VAR1"])
Expand Down Expand Up @@ -293,7 +293,7 @@ func Test_GetRadixSecretRefsAsEnvironmentVariables(t *testing.T) {

assert.NoError(t, err)
assert.Len(t, envVars, len(testCase.expectedEnvVars)+len(testCase.expectedSecrets)+len(testCase.expectedSecretRefsEnvVars))
envVarsConfigMap, envVarsConfigMapMetadata, err := testEnv.kubeUtil.GetOrCreateEnvVarsConfigMapAndMetadataMap(context.Background(), utils.GetEnvironmentNamespace(appName, env), appName, componentName)
envVarsConfigMap, envVarsConfigMapMetadata, err := testEnv.kubeUtil.GetOrCreateEnvVarsConfigMapAndMetadataMap(context.Background(), utils.GetEnvironmentNamespace(appName, envName), appName, componentName)
assert.NoError(t, err)
assert.NotNil(t, envVarsConfigMap)
assert.NotNil(t, envVarsConfigMapMetadata)
Expand Down Expand Up @@ -363,7 +363,7 @@ func Test_GetRadixSecretRefsAsEnvironmentVariables(t *testing.T) {

assert.NoError(t, err)
assert.Len(t, envVars, len(testCase.expectedEnvVars)+len(testCase.expectedSecrets)+len(testCase.expectedSecretRefsEnvVars))
envVarsConfigMap, envVarsConfigMapMetadata, err := testEnv.kubeUtil.GetOrCreateEnvVarsConfigMapAndMetadataMap(context.Background(), utils.GetEnvironmentNamespace(appName, env), appName, jobName)
envVarsConfigMap, envVarsConfigMapMetadata, err := testEnv.kubeUtil.GetOrCreateEnvVarsConfigMapAndMetadataMap(context.Background(), utils.GetEnvironmentNamespace(appName, envName), appName, jobName)
assert.NoError(t, err)
assert.NotNil(t, envVarsConfigMap)
assert.NotNil(t, envVarsConfigMapMetadata)
Expand Down
Loading

0 comments on commit f217ad6

Please sign in to comment.