pkg/providers/imagefamily/customscriptsbootstrap/provisionclientbootstrap.go (204 lines of code) (raw):
/*
Portions Copyright (c) Microsoft Corporation.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package customscriptsbootstrap
import (
"context"
"encoding/base64"
"fmt"
"log/slog"
"math"
"os"
"strings"
"time"
"github.com/Azure/aks-middleware/http/client/direct/restlogger"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/karpenter-provider-azure/pkg/apis/v1alpha2"
"github.com/Azure/karpenter-provider-azure/pkg/operator/options"
"github.com/Azure/karpenter-provider-azure/pkg/providers/imagefamily/bootstrap"
"github.com/Azure/karpenter-provider-azure/pkg/provisionclients/client"
"github.com/Azure/karpenter-provider-azure/pkg/provisionclients/client/operations"
"github.com/Azure/karpenter-provider-azure/pkg/provisionclients/models"
"github.com/Azure/karpenter-provider-azure/pkg/utils"
v1 "k8s.io/api/core/v1"
karpv1 "sigs.k8s.io/karpenter/pkg/apis/v1"
"sigs.k8s.io/karpenter/pkg/cloudprovider"
httptransport "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
"github.com/samber/lo"
"k8s.io/apimachinery/pkg/api/resource"
)
type ProvisionClientBootstrap struct {
ClusterName string
KubeletConfig *bootstrap.KubeletConfiguration
Taints []v1.Taint `hash:"set"`
StartupTaints []v1.Taint `hash:"set"`
Labels map[string]string `hash:"set"`
SubnetID string
Arch string
SubscriptionID string
ClusterResourceGroup string
ResourceGroup string
KubeletClientTLSBootstrapToken string
KubernetesVersion string
ImageDistro string
IsWindows bool
InstanceType *cloudprovider.InstanceType
StorageProfile string
ImageFamily string
}
var _ Bootstrapper = (*ProvisionClientBootstrap)(nil) // assert ProvisionClientBootstrap implements customscriptsbootstrapper
// nolint gocyclo - will be refactored later
func (p ProvisionClientBootstrap) GetCustomDataAndCSE(ctx context.Context) (string, string, error) {
if p.IsWindows {
// TODO(Windows)
return "", "", fmt.Errorf("windows is not supported")
}
labels := lo.Assign(map[string]string{}, p.Labels)
getAgentbakerGeneratedLabels(p.ResourceGroup, labels)
provisionProfile := &models.ProvisionProfile{
Name: lo.ToPtr(""),
Architecture: lo.ToPtr(lo.Ternary(p.Arch == karpv1.ArchitectureAmd64, "x64", "Arm64")),
OsType: lo.ToPtr(lo.Ternary(p.IsWindows, models.OSTypeWindows, models.OSTypeLinux)),
VMSize: lo.ToPtr(p.InstanceType.Name),
Distro: lo.ToPtr(p.ImageDistro),
CustomNodeLabels: labels,
OrchestratorVersion: lo.ToPtr(p.KubernetesVersion),
VnetSubnetID: lo.ToPtr(p.SubnetID),
StorageProfile: lo.ToPtr(p.StorageProfile),
NodeInitializationTaints: lo.Map(p.StartupTaints, func(taint v1.Taint, _ int) string { return taint.ToString() }),
NodeTaints: lo.Map(p.Taints, func(taint v1.Taint, _ int) string { return taint.ToString() }),
SecurityProfile: &models.AgentPoolSecurityProfile{
SSHAccess: lo.ToPtr(models.SSHAccessLocalUser),
// EnableVTPM: lo.ToPtr(false), // Unsupported as of now (Trusted launch)
// EnableSecureBoot: lo.ToPtr(false), // Unsupported as of now (Trusted launch)
},
MaxPods: lo.ToPtr(p.KubeletConfig.MaxPods),
VnetCidrs: []string{}, // Unsupported as of now; TODO(Windows)
// MessageOfTheDay: lo.ToPtr(""), // Unsupported as of now
// AgentPoolWindowsProfile: &models.AgentPoolWindowsProfile{}, // Unsupported as of now; TODO(Windows)
// KubeletDiskType: lo.ToPtr(models.KubeletDiskTypeUnspecified), // Unsupported as of now
// CustomLinuxOSConfig: &models.CustomLinuxOSConfig{}, // Unsupported as of now (sysctl)
// EnableFIPS: lo.ToPtr(false), // Unsupported as of now
// GpuInstanceProfile: lo.ToPtr(models.GPUInstanceProfileUnspecified), // Unsupported as of now (MIG)
// WorkloadRuntime: lo.ToPtr(models.WorkloadRuntimeUnspecified), // Unsupported as of now (Kata)
// ArtifactStreamingProfile: &models.ArtifactStreamingProfile{
// Enabled: lo.ToPtr(false), // Unsupported as of now
// },
}
switch p.ImageFamily {
case v1alpha2.Ubuntu2204ImageFamily:
provisionProfile.OsSku = to.Ptr(models.OSSKUUbuntu)
case v1alpha2.AzureLinuxImageFamily:
provisionProfile.OsSku = to.Ptr(models.OSSKUAzureLinux)
default:
provisionProfile.OsSku = to.Ptr(models.OSSKUUbuntu)
}
if p.KubeletConfig != nil {
provisionProfile.CustomKubeletConfig = &models.CustomKubeletConfig{
CPUCfsQuota: p.KubeletConfig.CPUCFSQuota,
ImageGcHighThreshold: p.KubeletConfig.ImageGCHighThresholdPercent,
ImageGcLowThreshold: p.KubeletConfig.ImageGCLowThresholdPercent,
ContainerLogMaxSizeMB: convertContainerLogMaxSizeToMB(p.KubeletConfig.ContainerLogMaxSize),
ContainerLogMaxFiles: p.KubeletConfig.ContainerLogMaxFiles,
PodMaxPids: convertPodMaxPids(p.KubeletConfig.PodPidsLimit),
}
// NodeClaim defaults don't work somehow and keep giving invalid values. Can be improved later.
if p.KubeletConfig.CPUCFSQuotaPeriod.Duration.String() != "0s" {
provisionProfile.CustomKubeletConfig.CPUCfsQuotaPeriod = lo.ToPtr(p.KubeletConfig.CPUCFSQuotaPeriod.Duration.String())
}
if p.KubeletConfig.CPUManagerPolicy != "" {
provisionProfile.CustomKubeletConfig.CPUManagerPolicy = lo.ToPtr(p.KubeletConfig.CPUManagerPolicy)
}
if p.KubeletConfig.TopologyManagerPolicy != "" {
provisionProfile.CustomKubeletConfig.TopologyManagerPolicy = lo.ToPtr(p.KubeletConfig.TopologyManagerPolicy)
}
if len(p.KubeletConfig.AllowedUnsafeSysctls) > 0 {
provisionProfile.CustomKubeletConfig.AllowedUnsafeSysctls = p.KubeletConfig.AllowedUnsafeSysctls
}
}
if modeString, ok := p.Labels["kubernetes.azure.com/mode"]; ok && modeString == "system" {
provisionProfile.Mode = lo.ToPtr(models.AgentPoolModeSystem)
} else {
provisionProfile.Mode = lo.ToPtr(models.AgentPoolModeUser)
}
if utils.IsNvidiaEnabledSKU(p.InstanceType.Name) {
provisionProfile.GpuProfile = &models.GPUProfile{
DriverType: lo.ToPtr(lo.Ternary(utils.UseGridDrivers(p.InstanceType.Name), models.DriverTypeGRID, models.DriverTypeCUDA)),
InstallGPUDriver: lo.ToPtr(true),
}
}
provisionHelperValues := &models.ProvisionHelperValues{
SkuCPU: lo.ToPtr(p.InstanceType.Capacity.Cpu().AsApproximateFloat64()),
SkuMemory: lo.ToPtr(math.Ceil(reverseVMMemoryOverhead(options.FromContext(ctx).VMMemoryOverheadPercent, p.InstanceType.Capacity.Memory().AsApproximateFloat64()) / 1024 / 1024 / 1024)),
}
return p.getNodeBootstrappingFromClient(ctx, provisionProfile, provisionHelperValues, p.KubeletClientTLSBootstrapToken)
}
func (p *ProvisionClientBootstrap) getNodeBootstrappingFromClient(ctx context.Context, provisionProfile *models.ProvisionProfile, provisionHelperValues *models.ProvisionHelperValues, bootstrapToken string) (string, string, error) {
transport := httptransport.New(options.FromContext(ctx).NodeBootstrappingServerURL, "/", []string{"http"})
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
loggingClient := restlogger.NewLoggingClient(logger)
transport.Transport = loggingClient.Transport
client := client.New(transport, strfmt.Default)
params := operations.NewNodeBootstrappingGetParams()
params.ResourceGroupName = p.ClusterResourceGroup
params.ResourceName = p.ClusterName
params.SubscriptionID = p.SubscriptionID
provisionValues := &models.ProvisionValues{
ProvisionProfile: provisionProfile,
ProvisionHelperValues: provisionHelperValues,
}
params.Parameters = provisionValues
params.WithTimeout(30 * time.Second)
params.Context = ctx
resp, err := client.Operations.NodeBootstrappingGet(params)
if err != nil {
// As of now we just fail the provisioning given the unlikely scenario of retriable error, but could be revisited along with retriable status on the server side.
return "", "", err
}
if resp.Payload == nil {
return "", "", fmt.Errorf("no payload in response")
}
if resp.Payload.Cse == nil || *resp.Payload.Cse == "" {
return "", "", fmt.Errorf("no CSE in response")
}
if resp.Payload.CustomData == nil || *resp.Payload.CustomData == "" {
return "", "", fmt.Errorf("no CustomData in response")
}
cseWithoutBootstrapToken := *resp.Payload.Cse
customDataWithoutBootstrapToken := *resp.Payload.CustomData
cseWithBootstrapToken := strings.ReplaceAll(cseWithoutBootstrapToken, "{{.TokenID}}.{{.TokenSecret}}", bootstrapToken)
decodedCustomDataWithoutBootstrapTokenInBytes, err := base64.StdEncoding.DecodeString(customDataWithoutBootstrapToken)
if err != nil {
return "", "", err
}
decodedCustomDataWithBootstrapToken := strings.ReplaceAll(string(decodedCustomDataWithoutBootstrapTokenInBytes), "{{.TokenID}}.{{.TokenSecret}}", bootstrapToken)
customDataWithBootstrapToken := base64.StdEncoding.EncodeToString([]byte(decodedCustomDataWithBootstrapToken))
return customDataWithBootstrapToken, cseWithBootstrapToken, nil
}
func getAgentbakerGeneratedLabels(nodeResourceGroup string, nodeLabels map[string]string) {
// Delegatable defaulting?
nodeLabels["kubernetes.azure.com/role"] = "agent"
nodeLabels["kubernetes.azure.com/cluster"] = normalizeResourceGroupNameForLabel(nodeResourceGroup)
}
func normalizeResourceGroupNameForLabel(resourceGroupName string) string {
truncated := resourceGroupName
truncated = strings.ReplaceAll(truncated, "(", "-")
truncated = strings.ReplaceAll(truncated, ")", "-")
const maxLen = 63
if len(truncated) > maxLen {
truncated = truncated[0:maxLen]
}
if strings.HasSuffix(truncated, "-") ||
strings.HasSuffix(truncated, "_") ||
strings.HasSuffix(truncated, ".") {
if len(truncated) > 62 {
return truncated[0:len(truncated)-1] + "z"
}
return truncated + "z"
}
return truncated
}
func reverseVMMemoryOverhead(vmMemoryOverheadPercent float64, adjustedMemory float64) float64 {
// This is not the best way to do it... But will be refactored later, given that retrieving the original memory properly might involves some restructure.
// Due to the fact that it is abstracted behind the cloudprovider interface.
return adjustedMemory / (1 - vmMemoryOverheadPercent)
}
func convertContainerLogMaxSizeToMB(containerLogMaxSize string) *int32 {
q, err := resource.ParseQuantity(containerLogMaxSize)
if err == nil {
// This could be improved later
return lo.ToPtr(int32(math.Round(q.AsApproximateFloat64() / 1024 / 1024)))
}
return nil
}
func convertPodMaxPids(podPidsLimit *int64) *int32 {
if podPidsLimit != nil {
podPidsLimitInt64 := *podPidsLimit
if podPidsLimitInt64 > int64(math.MaxInt32) {
// This could be improved later
return lo.ToPtr(int32(math.MaxInt32))
} else if podPidsLimitInt64 < 0 {
// This as well
return lo.ToPtr(int32(-1))
} else {
return lo.ToPtr(int32(podPidsLimitInt64)) // golint:ignore G115 already check overflow
}
}
return nil
}