Skip to content

Commit

Permalink
[FEATURE] - Exposing Integrations (#180)
Browse files Browse the repository at this point in the history
* [FEATURE] - Exposing Integrations

At present the reports by infracost and checkov are only available in the controller namespace which make reporting on them difficult, beyond the pod logs. In the intention is to have a a tnctl command (a kubectl plugin) and allow uses to explain / describe the configuration resource
  • Loading branch information
gambol99 committed Jul 6, 2022
1 parent 61dbeec commit d836220
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 78 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -2,6 +2,7 @@
*.orig
.idea
.vscode
.vim
gcp-auth.json
/*.env
#local dev artifacts
Expand Down
101 changes: 77 additions & 24 deletions pkg/controller/configuration/ensure.go
Expand Up @@ -570,37 +570,65 @@ func (c *Controller) ensureCostStatus(configuration *terraformv1alphav1.Configur
return reconcile.Result{}, nil
}

input := secret.Data["costs.json"]
if len(input) == 0 {
return reconcile.Result{}, nil
}

// @step: parse the cost report json
if secret.Data["costs.json"] != nil {
report := make(map[string]interface{})
if err := json.NewDecoder(bytes.NewReader(secret.Data["costs.json"])).Decode(&report); err != nil {
cond.Failed(err, "Failed to decode the terraform costs report")
values := map[string]float64{
"totalMonthlyCost": 0,
"totalHourlyCost": 0,
}
for key := range values {
value := gjson.GetBytes(input, key)
if !value.Exists() {
cond.ActionRequired("Cost report does not include the %s value", key)

return reconcile.Result{}, err
return reconcile.Result{}, controller.ErrIgnore
}

var monthly, hourly float64
cost, err := strconv.ParseFloat(value.String(), 64)
if err != nil {
cond.ActionRequired("Cost report contains an include value: %q for item: %s", value.String(), key)

if v, ok := report["totalMonthlyCost"].(string); ok {
if x, err := strconv.ParseFloat(v, 64); err == nil {
monthly = x
}
}
if v, ok := report["totalHourlyCost"].(string); ok {
if x, err := strconv.ParseFloat(v, 64); err == nil {
hourly = x
}
return reconcile.Result{}, controller.ErrIgnore
}
values[key] = cost
}

configuration.Status.Costs = &terraformv1alphav1.CostStatus{
Enabled: true,
Hourly: fmt.Sprintf("$%v", hourly),
Monthly: fmt.Sprintf("$%v", monthly),
}
configuration.Status.Costs = &terraformv1alphav1.CostStatus{
Enabled: true,
Hourly: fmt.Sprintf("$%v", values["totalHourlyCost"]),
Monthly: fmt.Sprintf("$%v", values["totalMonthlyCost"]),
}

// @step: update the prometheus metrics
monthlyCostMetric.WithLabelValues(labels...).Set(monthly)
hourlyCostMetric.WithLabelValues(labels...).Set(hourly)
// @step: update the prometheus metrics
monthlyCostMetric.WithLabelValues(labels...).Set(values["totalMonthlyCost"])
hourlyCostMetric.WithLabelValues(labels...).Set(values["totalHourlyCost"])

// @step: copy the infracost report into the configuration namespace
copied := &v1.Secret{}
copied.Namespace = configuration.GetNamespace()
copied.Name = configuration.GetTerraformCostSecretName()
copied.Labels = map[string]string{
terraformv1alphav1.ConfigurationNameLabel: configuration.GetName(),
terraformv1alphav1.ConfigurationUIDLabel: string(configuration.GetUID()),
}
copied.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: terraformv1alphav1.SchemeGroupVersion.String(),
Kind: terraformv1alphav1.ConfigurationKind,
Name: configuration.GetName(),
UID: configuration.GetUID(),
},
}
copied.Data = secret.Data

if err := kubernetes.CreateOrForceUpdate(ctx, c.cc, copied); err != nil {
cond.Failed(err, "Failed to create or update the terraform costs secret")

return reconcile.Result{}, err
}

return reconcile.Result{}, nil
Expand All @@ -610,6 +638,7 @@ func (c *Controller) ensureCostStatus(configuration *terraformv1alphav1.Configur
// ensurePolicyStatus is responsible for checking the checkov results and refusing to continue if failed
func (c *Controller) ensurePolicyStatus(configuration *terraformv1alphav1.Configuration, state *state) controller.EnsureFunc {
cond := controller.ConditionMgr(configuration, terraformv1alphav1.ConditionTerraformPolicy, c.recorder)
key := "results_json.json"

return func(ctx context.Context) (reconcile.Result, error) {
switch {
Expand Down Expand Up @@ -637,7 +666,7 @@ func (c *Controller) ensurePolicyStatus(configuration *terraformv1alphav1.Config
}

// @step: retrieve summary from the report
checksFailed := gjson.GetBytes(secret.Data["results_json.json"], "summary.failed")
checksFailed := gjson.GetBytes(secret.Data[key], "summary.failed")
if !checksFailed.Exists() {
cond.Failed(errors.New("missing report"), "Security report does not contain a summary of finding, please contact platform administrator")

Expand All @@ -650,6 +679,30 @@ func (c *Controller) ensurePolicyStatus(configuration *terraformv1alphav1.Config
return reconcile.Result{}, controller.ErrIgnore
}

// @step: copy the report into the configuration namespace
copied := &v1.Secret{}
copied.Namespace = configuration.GetNamespace()
copied.Name = configuration.GetTerraformPolicySecretName()
copied.Labels = map[string]string{
terraformv1alphav1.ConfigurationNameLabel: configuration.GetName(),
terraformv1alphav1.ConfigurationUIDLabel: string(configuration.GetUID()),
}
copied.OwnerReferences = []metav1.OwnerReference{
{
APIVersion: terraformv1alphav1.SchemeGroupVersion.String(),
Kind: terraformv1alphav1.ConfigurationKind,
Name: configuration.GetName(),
UID: configuration.GetUID(),
},
}
copied.Data = secret.Data

if err := kubernetes.CreateOrForceUpdate(ctx, c.cc, copied); err != nil {
cond.Failed(err, "Failed to create or update the terraform policy secret")

return reconcile.Result{}, err
}

if checksFailed.Int() > 0 {
cond.ActionRequired("Configuration has failed security policy, refusing to continue")

Expand Down
35 changes: 35 additions & 0 deletions pkg/controller/configuration/reconcile_test.go
Expand Up @@ -680,6 +680,19 @@ var _ = Describe("Configuration Controller", func() {
Expect(configuration.Status.Costs.Monthly).To(Equal("$100"))
Expect(configuration.Status.Costs.Hourly).To(Equal("$0.01"))
})

It("should have copied the secret into the configuration namespace", func() {
expected := "\n{\n\t\"totalHourlyCost\": \"0.01\",\n \"totalMonthlyCost\": \"100.00\"\n}\n"
secret := &v1.Secret{}
secret.Namespace = configuration.Namespace
secret.Name = configuration.GetTerraformCostSecretName()
found, err := kubernetes.GetIfExists(context.TODO(), cc, secret)

Expect(err).ToNot(HaveOccurred())
Expect(found).To(BeTrue())
Expect(secret.Data).To(HaveKey("costs.json"))
Expect(string(secret.Data["costs.json"])).To(Equal(expected))
})
})
})

Expand Down Expand Up @@ -1447,6 +1460,17 @@ terraform {
Expect(cc.List(context.TODO(), list, client.InNamespace(ctrl.ControllerNamespace))).ToNot(HaveOccurred())
Expect(len(list.Items)).To(Equal(1))
})

It("should copied the report into the configuration namespace", func() {
secret := &v1.Secret{}
secret.Namespace = configuration.Namespace
secret.Name = configuration.GetTerraformPolicySecretName()
found, err := kubernetes.GetIfExists(context.TODO(), ctrl.cc, secret)
Expect(err).ToNot(HaveOccurred())
Expect(found).To(BeTrue())
Expect(secret.Data).To(HaveKey("results_json.json"))
Expect(string(secret.Data["results_json.json"])).To(ContainSubstring("summary"))
})
})

When("policy report contains no failed checks", func() {
Expand Down Expand Up @@ -1483,6 +1507,17 @@ terraform {
Expect(cc.List(context.TODO(), list, client.InNamespace(ctrl.ControllerNamespace))).ToNot(HaveOccurred())
Expect(len(list.Items)).To(Equal(2))
})

It("should copied the report into the configuration namespace", func() {
secret := &v1.Secret{}
secret.Namespace = configuration.Namespace
secret.Name = configuration.GetTerraformPolicySecretName()
found, err := kubernetes.GetIfExists(context.TODO(), ctrl.cc, secret)
Expect(err).ToNot(HaveOccurred())
Expect(found).To(BeTrue())
Expect(secret.Data).To(HaveKey("results_json.json"))
Expect(string(secret.Data["results_json.json"])).To(ContainSubstring("summary"))
})
})
})
})
Expand Down
1 change: 0 additions & 1 deletion test/e2e/check-suite.sh
Expand Up @@ -55,7 +55,6 @@ run_checks() {
"${UNITS}/cloud/${CLOUD}/provider.bats"
"${UNITS}/cloud/${CLOUD}/plan.bats"
"${UNITS}/plan.bats"
"${UNITS}/costs.bats"
"${UNITS}/apply.bats"
"${UNITS}/cloud/${CLOUD}/confirm.bats"
"${UNITS}/drift.bats"
Expand Down
9 changes: 9 additions & 0 deletions test/e2e/integration/checkov.bats
Expand Up @@ -104,6 +104,15 @@ EOF
[[ "$status" -eq 0 ]]
}

@test "We should have a copy the policy report in the configuration namespace" {
UUID=$(kubectl -n ${APP_NAMESPACE} get configuration ${RESOURCE_NAME} -o json | jq -r '.metadata.uid')
[[ "$status" -eq 0 ]]
runit "kubectl -n ${APP_NAMESPACE} get secret policy-${UUID}"
[[ "$status" -eq 0 ]]
runit "kubectl -n ${APP_NAMESPACE} get secret policy-${UUID} -o json" "jq -r '.data.results_json.json'"
[[ "$status" -eq 0 ]]
}

@test "We should see the conditions indicate the configuration failed policy" {
POD=$(kubectl -n ${APP_NAMESPACE} get pod -l terraform.appvia.io/configuration=${RESOURCE_NAME} -l terraform.appvia.io/stage=plan -o json | jq -r '.items[0].metadata.name')
[[ "$status" -eq 0 ]]
Expand Down
5 changes: 3 additions & 2 deletions test/e2e/integration/cloud/aws/infracost.bats
Expand Up @@ -18,15 +18,16 @@
load ../../../lib/helper

setup() {
[[ ! -f ${BATS_PARENT_TMPNAME}.skip ]] || skip "skip remaining tests"
[[ ! -f "${BATS_PARENT_TMPNAME}.skip" ]] || skip "skip remaining tests"
}

teardown() {
[[ -n "$BATS_TEST_COMPLETED" ]] || touch ${BATS_PARENT_TMPNAME}.skip
}

@test "We should have a token for the infracost integration" {
[[ -z ${INFRACOST_API_KEY} ]] && touch ${BATS_PARENT_TMPNAME}.skip
[[ -z "${INFRACOST_API_KEY}" ]] && touch ${BATS_PARENT_TMPNAME}.skip
[[ "$status" -eq 0 ]]
}

@test "We should be able to create a configuration which costs money on aws" {
Expand Down
51 changes: 0 additions & 51 deletions test/e2e/integration/costs.bats

This file was deleted.

32 changes: 32 additions & 0 deletions test/e2e/integration/plan.bats
Expand Up @@ -85,3 +85,35 @@ teardown() {
runit "kubectl -n ${APP_NAMESPACE} logs ${POD} 2>&1" "grep -q '\[build\] completed'"
[[ "$status" -eq 0 ]]
}

@test "We should have a secret in the terraform namespace containing the report" {
UUID=$(kubectl -n ${APP_NAMESPACE} get configuration ${RESOURCE_NAME} -o json | jq -r '.metadata.uid')
[[ "$status" -eq 0 ]]

runit "kubectl -n ${NAMESPACE} get secret costs-${UUID}"
[[ "$status" -eq 0 ]]
runit "kubectl -n ${NAMESPACE} get secret costs-${UUID} -o json" "jq -r '.data[\"costs.json\"]'"
[[ "$status" -eq 0 ]]
}

@test "We should see the cost integration is enabled" {
runit "kubectl -n ${APP_NAMESPACE} get configuration ${RESOURCE_NAME} -o json" "jq -r '.status.costs.enabled' | grep -q true"
[[ "$status" -eq 0 ]]
}

@test "We should see the cost associated to the configuration" {
runit "kubectl -n ${APP_NAMESPACE} get configuration ${RESOURCE_NAME} -o json" "jq -r '.status.costs.monthly' | grep -q '\$0'"
[[ "$status" -eq 0 ]]
runit "kubectl -n ${APP_NAMESPACE} get configuration ${RESOURCE_NAME} -o json" "jq -r '.status.costs.hourly' | grep -q '\$0'"
[[ "$status" -eq 0 ]]
}

@test "We should have a copy of the infracost report in the configuration namespace" {
UUID=$(kubectl -n ${APP_NAMESPACE} get configuration ${RESOURCE_NAME} -o json | jq -r '.metadata.uid')
[[ "$status" -eq 0 ]]

runit "kubectl -n ${APP_NAMESPACE} get secret costs-${UUID}"
[[ "$status" -eq 0 ]]
runit "kubectl -n ${APP_NAMESPACE} get secret costs-${UUID} -o json" "jq -r '.data[\"costs.json\"]'"
[[ "$status" -eq 0 ]]
}

0 comments on commit d836220

Please sign in to comment.