Skip to content

Commit

Permalink
Add aks support (#100)
Browse files Browse the repository at this point in the history
Adding support for tagging pvc's in AKS.
  • Loading branch information
boris-smidt-klarrio authored Dec 5, 2024
1 parent faf40c4 commit 618c018
Show file tree
Hide file tree
Showing 9 changed files with 494 additions and 26 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
# vendor/

cosign.*
.idea
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ RUN addgroup -S k8s-pvc-tagger && adduser -S k8s-pvc-tagger -G k8s-pvc-tagger
FROM scratch
COPY --from=builder /etc/passwd /etc/passwd
USER k8s-pvc-tagger
ENV APP_NAME=k8s-pvc-tagger

# https://github.com/aws/aws-sdk-go/issues/2322
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/${APP_NAME} /
Expand Down
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,13 @@ metadata:
### Multi-cloud support
Currently supported clouds: AWS, GCP.
Currently supported clouds: AWS, GCP, Azure
Only one mode is active at a given time. Specify the cloud `k8s-pvc-tagger` is running in with the `--cloud` flag. Either `aws` or `gcp`.

If not specified `--cloud aws` is the default mode.

> NOTE: GCP labels have constraints that do not match the contraints allowed by Kubernetes labels. When running in GCP mode labels will be modified to fit GCP's constraints, if necessary. The main difference is `.` and `/` are not allowed, so a label such as `dom.tld/key` will be converted to `dom-tld_key`.
> NOTE: GCP labels have constraints that do not match the constraints allowed by Kubernetes labels. When running in GCP mode labels will be modified to fit GCP's constraints, if necessary. The main difference is `.` and `/` are not allowed, so a label such as `dom.tld/key` will be converted to `dom-tld_key`.

### Installation

Expand Down Expand Up @@ -117,6 +117,20 @@ gcloud iam roles create CustomDiskRole \
--stage="GA"
```

#### Azure rule
The [default role `Tag Contributor`](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/management-and-governance#tag-contributor) can be used to configure the access rights for the pvc-tagger.
At the moment this only supports csi-volumes are supported.
Because the kubernetes tags are richer than what you can set in azure we sanitize the tags for you:

- The invalid characters in key are replaced with `_`: `<>%&\?/`
This results in `Kubernetes/Cluster` to become `Kubernetes_Cluster`.
- tags longer than to 512 characters are truncated

We generate an error in case there any of these limits are breached:
- tag values are limited to 256 characters
- the tag count is limited to 50 tags
- when a tag after sanitization collides with another tag, `Kubernetes_Cluster` and `Kubernetes/Cluster`

#### Install via helm

```
Expand Down
177 changes: 177 additions & 0 deletions azure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package main

import (
"context"
"errors"
"fmt"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"maps"
"strings"
)

var (
ErrAzureTooManyTags error = errors.New("Only up to 50 tags can be set on an azure resource")
ErrAzureValueToLong error = errors.New("A value can only contain 256 characters")
ErrAzureDuplicatedTags error = errors.New("There are duplicated keys after sanitization")
)

type DiskTags = map[string]*string
type AzureSubscription = string

type AzureClient interface {
GetDiskTags(ctx context.Context, subscription AzureSubscription, resourceGroupName string, diskName string) (DiskTags, error)
SetDiskTags(ctx context.Context, subscription AzureSubscription, resourceGroupName string, diskName string, tags DiskTags) error
}

type azureClient struct {
client *armresources.TagsClient
}

func NewAzureClient() (AzureClient, error) {
creds, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
return nil, err
}
client, err := armresources.NewTagsClient("", creds, &arm.ClientOptions{})
if err != nil {
return nil, err
}

return azureClient{client}, err
}

func diskScope(subscription string, resourceGroupName string, diskName string) string {
return fmt.Sprintf("subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/disks/%s", subscription, resourceGroupName, diskName)
}

func (self azureClient) GetDiskTags(ctx context.Context, subscription AzureSubscription, resourceGroupName string, diskName string) (DiskTags, error) {

tags, err := self.client.GetAtScope(ctx, diskScope(subscription, resourceGroupName, diskName), &armresources.TagsClientGetAtScopeOptions{})
if err != nil {
return nil, fmt.Errorf("could not get the tags for: %w", err)
}

return tags.Properties.Tags, nil
}

func (self azureClient) SetDiskTags(ctx context.Context, subscription AzureSubscription, resourceGroupName string, diskName string, tags DiskTags) error {
response, err := self.client.UpdateAtScope(
ctx,
diskScope(subscription, resourceGroupName, diskName),
armresources.TagsPatchResource{
to.Ptr(armresources.TagsPatchOperationReplace),
&armresources.Tags{Tags: tags},
}, &armresources.TagsClientUpdateAtScopeOptions{},
)
if err != nil {
return fmt.Errorf("could not set the tags for: %w", err)
}
log.WithFields(log.Fields{"disk": diskName, "resource-group": resourceGroupName}).Debugf("updated disk tags to tags=%v", response.Properties.Tags)
return nil
}

func parseAzureVolumeID(volumeID string) (subscription string, resourceGroup string, diskName string, err error) {
// '/subscriptions/{subscription}/resourceGroups/{resourceGroup}/providers/Microsoft.Compute/disks/{diskname}"'
fields := strings.Split(volumeID, "/")
if len(fields) != 9 {
return "", "", "", errors.New("invalid volume id")
}
subscription = fields[2]
resourceGroup = fields[4]
diskName = fields[8]
return subscription, resourceGroup, diskName, nil
}

func sanitizeLabelsForAzure(tags map[string]string) (DiskTags, error) {
diskTags := make(DiskTags)
if len(tags) > 50 {
return nil, ErrAzureTooManyTags
}
for k, v := range tags {
sanitizedKey := sanitizeKeyForAzure(k)
value, err := sanitizeValueForAzure(v)
if err != nil {
return nil, err
}

if _, ok := diskTags[sanitizedKey]; ok {
return nil, fmt.Errorf("tag is duplicated after sanitization colliding tags key=%s sanitized-key=%s: %w", k, sanitizedKey, ErrAzureDuplicatedTags)
}
diskTags[sanitizedKey] = &value
}

return diskTags, nil
}

func sanitizeKeyForAzure(s string) string {
// remove forbidden characters
if strings.ContainsAny(s, `<>%&\?/`) {
for _, c := range `<>%&\?/` {
s = strings.ReplaceAll(s, string(c), "_")
}
}

// truncate the key the max length for azure
if len(s) > 512 {
s = s[:512]
}

return s
}

func sanitizeValueForAzure(s string) (string, error) {
// the value can contain at most 256 characters
if len(s) > 256 {
return "", fmt.Errorf("%s value is invalid: %w", s, ErrAzureValueToLong)
}
return s, nil
}

func UpdateAzureVolumeTags(ctx context.Context, client AzureClient, volumeID string, tags map[string]string, removedTags []string, storageclass string) error {
sanitizedLabels, err := sanitizeLabelsForAzure(tags)
if err != nil {
return err
}

log.Debugf("labels to add to PD volume: %s: %v", volumeID, sanitizedLabels)
subscription, resourceGroup, diskName, err := parseAzureVolumeID(volumeID)
if err != nil {
return err
}

existingTags, err := client.GetDiskTags(ctx, subscription, resourceGroup, diskName)
if err != nil {
return err
}

// merge existing disk labels with new labels:
updatedTags := make(DiskTags)
if existingTags != nil {
updatedTags = maps.Clone(existingTags)
}
maps.Copy(updatedTags, sanitizedLabels)

for _, tag := range removedTags {
delete(updatedTags, tag)
}

if maps.Equal(existingTags, updatedTags) {
log.Debug("labels already set on PD")
return nil
}

err = client.SetDiskTags(ctx, subscription, resourceGroup, diskName, updatedTags)
if err != nil {
promActionsTotal.With(prometheus.Labels{"status": "error", "storageclass": storageclass}).Inc()
return err
}

log.Debug("successfully set labels on PD")
promActionsTotal.With(prometheus.Labels{"status": "success", "storageclass": storageclass}).Inc()
return nil
}
Loading

0 comments on commit 618c018

Please sign in to comment.