Skip to content

Commit

Permalink
Add kubectl apply (#10)
Browse files Browse the repository at this point in the history
* Add k8s client

* Add kubectl delete

* Fix datasource tests

* Update Readme
  • Loading branch information
nikita-akuity authored Jan 24, 2023
1 parent 57a1d43 commit c7989c7
Show file tree
Hide file tree
Showing 16 changed files with 1,250 additions and 86 deletions.
56 changes: 9 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,22 @@ With this provider you can manage Argo CD instances and clusters on [Akuity Plat
- [Terraform](https://www.terraform.io/downloads.html) >= 1.0

## Typical use case
Add a new cluster `test-cluster` to the existing Argo CD instance `manualy-created` and then install [the agent](https://docs.akuity.io/akuity-platform/agent) to the configured cluster.
Add a new cluster `test-cluster` to the existing Argo CD instance `manualy-created` and install [the agent](https://docs.akuity.io/akuity-platform/agent) to the configured cluster.

1. Create an API key for your organization
* Use `Admin` role for the key
1. Configure Environment variables
2. Configure Environment variables
```shell
export AKUITY_API_KEY_ID=<key-id>
export AKUITY_API_KEY_SECRET=<key-secret>
```
1. Use this or similar configuration:
3. Use this or similar configuration:
```hcl
terraform {
required_providers {
akp = {
source = "akuity/akp"
version = "~> 0.1"
}
kubectl = {
source = "gavinbunney/kubectl"
version = "~> 1.14"
version = "~> 0.2"
}
}
}
Expand All @@ -44,55 +40,21 @@ With this provider you can manage Argo CD instances and clusters on [Akuity Plat
org_name = "<organization-name>"
}
provider "kubectl" {
# Configure the kubectl provider
# to connect to the existing kubernetes cluster
}
# Read Existing Argo CD Instance
# Read the existing Argo CD Instance
data "akp_instance" "existing" {
name = "manualy-created"
}
# Add cluster to the existing instance
# Add cluster to the existing instance and install the agent
resource "akp_cluster" "test" {
name = "test-cluster"
description = "Test Cluster 1"
namespace = "akuity"
instance_id = data.akp_instance.existing.id
kube_config = {
# Configuration similar to `kubernetes` provider
}
}
# Split and install agent manifests
data "kubectl_file_documents" "agent" {
content = akp_cluster.test.manifests
}
# Create namespace first
resource "kubectl_manifest" "agent_namespace" {
yaml_body = lookup(data.kubectl_file_documents.agent.manifests, "/api/v1/namespaces/${akp_cluster.test.namespace}/namespaces/${akp_cluster.test.namespace}")
wait = true
}
# Create everything else
resource "kubectl_manifest" "agent" {
for_each = data.kubectl_file_documents.agent.manifests
yaml_body = each.value
# Important!
wait_for_rollout = false
depends_on = [
kubectl_manifest.agent_namespace
]
}
```
1. First create the cluster using `-target`
```shell
terraform apply -target akp_cluster.test
```
This has to be done first, because terraform is unable to apply an unknown number of manifests in `for_each` loop, and the manifests only available *after* the cluster is created

1. Then you can apply the manifests
```shell
terraform apply
```
71 changes: 71 additions & 0 deletions akp/data_source_akp_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,77 @@ func (d *AkpClusterDataSource) Schema(ctx context.Context, req datasource.Schema
MarkdownDescription: "Cluster Annotations",
Computed: true,
},
"agent_version": schema.StringAttribute{
MarkdownDescription: "Installed agent version",
Computed: true,
},
"kube_config": schema.SingleNestedAttribute{
MarkdownDescription: "Kubernetes connection setings. If configured, terraform will try to connect to the cluster and install the agent",
Optional: true,
Attributes: map[string]schema.Attribute{
"host": schema.StringAttribute{
Optional: true,
Description: "The hostname (in form of URI) of Kubernetes master.",

},
"username": schema.StringAttribute{
Optional: true,
Description: "The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint.",
},
"password": schema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint.",
},
"insecure": schema.BoolAttribute{
Optional: true,
Description: "Whether server should be accessed without verifying the TLS certificate.",
},
"client_certificate": schema.StringAttribute{
Optional: true,
Description: "PEM-encoded client certificate for TLS authentication.",
},
"client_key": schema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "PEM-encoded client certificate key for TLS authentication.",
},
"cluster_ca_certificate": schema.StringAttribute{
Optional: true,
Description: "PEM-encoded root certificates bundle for TLS authentication.",
},
"config_paths": schema.ListAttribute{
ElementType: types.StringType,
Optional: true,
Description: "A list of paths to kube config files. Can be set with KUBE_CONFIG_PATHS environment variable.",
},
"config_path": schema.StringAttribute{
Optional: true,
Description: "Path to the kube config file.",
},
"config_context": schema.StringAttribute{
Optional: true,
Description: "Context name to load from the kube config file.",
},
"config_context_auth_info": schema.StringAttribute{
Optional: true,
Description: "",
},
"config_context_cluster": schema.StringAttribute{
Optional: true,
Description: "",
},
"token": schema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "Token to authenticate an service account",
},
"proxy_url": schema.StringAttribute{
Optional: true,
Description: "URL to the proxy to be used for all API requests",
},
},
},
},
}
}
Expand Down
71 changes: 71 additions & 0 deletions akp/data_source_akp_clusters.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,77 @@ func (d *AkpClustersDataSource) Schema(ctx context.Context, req datasource.Schem
MarkdownDescription: "Cluster Annotations",
Computed: true,
},
"agent_version": schema.StringAttribute{
MarkdownDescription: "Installed agent version",
Computed: true,
},
"kube_config": schema.SingleNestedAttribute{
MarkdownDescription: "Kubernetes connection setings. If configured, terraform will try to connect to the cluster and install the agent",
Optional: true,
Attributes: map[string]schema.Attribute{
"host": schema.StringAttribute{
Optional: true,
Description: "The hostname (in form of URI) of Kubernetes master.",

},
"username": schema.StringAttribute{
Optional: true,
Description: "The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint.",
},
"password": schema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint.",
},
"insecure": schema.BoolAttribute{
Optional: true,
Description: "Whether server should be accessed without verifying the TLS certificate.",
},
"client_certificate": schema.StringAttribute{
Optional: true,
Description: "PEM-encoded client certificate for TLS authentication.",
},
"client_key": schema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "PEM-encoded client certificate key for TLS authentication.",
},
"cluster_ca_certificate": schema.StringAttribute{
Optional: true,
Description: "PEM-encoded root certificates bundle for TLS authentication.",
},
"config_paths": schema.ListAttribute{
ElementType: types.StringType,
Optional: true,
Description: "A list of paths to kube config files. Can be set with KUBE_CONFIG_PATHS environment variable.",
},
"config_path": schema.StringAttribute{
Optional: true,
Description: "Path to the kube config file.",
},
"config_context": schema.StringAttribute{
Optional: true,
Description: "Context name to load from the kube config file.",
},
"config_context_auth_info": schema.StringAttribute{
Optional: true,
Description: "",
},
"config_context_cluster": schema.StringAttribute{
Optional: true,
Description: "",
},
"token": schema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "Token to authenticate an service account",
},
"proxy_url": schema.StringAttribute{
Optional: true,
Description: "URL to the proxy to be used for all API requests",
},
},
},
},
},
},
Expand Down
120 changes: 120 additions & 0 deletions akp/kube/apply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package kube

import (
"bytes"
"context"
"encoding/json"
"strings"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/printers"
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/dynamic"
"k8s.io/kubectl/pkg/cmd/apply"
"k8s.io/kubectl/pkg/cmd/delete"
cmdutil "k8s.io/kubectl/pkg/cmd/util"

)

type ApplyOpts struct {
DryRunStrategy cmdutil.DryRunStrategy
Force bool
Validate bool
}

// ApplyResource performs an apply of a unstructured resource
func (k *Kubectl) ApplyResource(ctx context.Context, obj *unstructured.Unstructured, applyOpts ApplyOpts) (string, error) {
objBytes, err := json.Marshal(obj)
if err != nil {
return "", err
}
ioStreams := genericclioptions.IOStreams{
In: &bytes.Buffer{},
Out: &bytes.Buffer{},
ErrOut: &bytes.Buffer{},
}
path, err := writeFile(objBytes)
if err != nil {
return "", err
}
defer deleteFile(path)
kubeApplyOpts, err := k.newApplyOptions(ioStreams, obj, path, applyOpts)
if err != nil {
return "", err
}
applyErr := kubeApplyOpts.Run()
var out []string
if buf := strings.TrimSpace(ioStreams.Out.(*bytes.Buffer).String()); len(buf) > 0 {
out = append(out, buf)
}
if buf := strings.TrimSpace(ioStreams.ErrOut.(*bytes.Buffer).String()); len(buf) > 0 {
out = append(out, buf)
}
return strings.Join(out, ". "), applyErr
}

func (k *Kubectl) newApplyOptions(ioStreams genericclioptions.IOStreams, obj *unstructured.Unstructured, path string, applyOpts ApplyOpts) (*apply.ApplyOptions, error) {
flags := apply.NewApplyFlags(k.fact, ioStreams)
o := &apply.ApplyOptions{
IOStreams: ioStreams,
VisitedUids: sets.NewString(),
VisitedNamespaces: sets.NewString(),
Recorder: genericclioptions.NoopRecorder{},
PrintFlags: flags.PrintFlags,
Overwrite: true,
OpenAPIPatch: true,
}
dynamicClient, err := dynamic.NewForConfig(k.config)
if err != nil {
return nil, err
}
o.DynamicClient = dynamicClient
o.DeleteOptions, err = delete.NewDeleteFlags("").ToOptions(dynamicClient, ioStreams)
if err != nil {
return nil, err
}
o.OpenAPISchema, err = k.OpenAPISchema()
if err != nil {
return nil, err
}
o.DryRunVerifier = resource.NewQueryParamVerifier(dynamicClient, k.fact.OpenAPIGetter(), resource.QueryParamFieldValidation)
validateDirective := metav1.FieldValidationIgnore
if applyOpts.Validate {
validateDirective = metav1.FieldValidationStrict
}
o.Validator, err = k.fact.Validator(validateDirective, o.DryRunVerifier)
if err != nil {
return nil, err
}
o.Builder = k.fact.NewBuilder()
o.Mapper, err = k.fact.ToRESTMapper()
if err != nil {
return nil, err
}

o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) {
o.PrintFlags.NamePrintFlags.Operation = operation
switch o.DryRunStrategy {
case cmdutil.DryRunClient:
err = o.PrintFlags.Complete("%s (dry run)")
if err != nil {
return nil, err
}
case cmdutil.DryRunServer:
err = o.PrintFlags.Complete("%s (server dry run)")
if err != nil {
return nil, err
}
}
return o.PrintFlags.ToPrinter()
}
o.DeleteOptions.FilenameOptions.Filenames = []string{path}
o.Namespace = obj.GetNamespace()
o.DeleteOptions.ForceDeletion = applyOpts.Force
o.DryRunStrategy = applyOpts.DryRunStrategy
return o, nil
}

Loading

0 comments on commit c7989c7

Please sign in to comment.