cli/azd/pkg/project/service_target_aks.go (665 lines of code) (raw):
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package project
import (
"context"
"errors"
"fmt"
"log"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/azure/azure-dev/cli/azd/pkg/alpha"
"github.com/azure/azure-dev/cli/azd/pkg/async"
"github.com/azure/azure-dev/cli/azd/pkg/azapi"
"github.com/azure/azure-dev/cli/azd/pkg/azure"
"github.com/azure/azure-dev/cli/azd/pkg/convert"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/ext"
"github.com/azure/azure-dev/cli/azd/pkg/helm"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/kubelogin"
"github.com/azure/azure-dev/cli/azd/pkg/kustomize"
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/tools"
"github.com/azure/azure-dev/cli/azd/pkg/tools/kubectl"
"github.com/sethvargo/go-retry"
)
const (
defaultDeploymentPath = "manifests"
)
var (
featureHelm alpha.FeatureId = alpha.MustFeatureKey("aks.helm")
featureKustomize alpha.FeatureId = alpha.MustFeatureKey("aks.kustomize")
// Finds URLS in the endpoints that contain additional metadata
// Example: http://10.0.101.18:80 (Service: todo-api, Type: ClusterIP)
endpointRegex = regexp.MustCompile(`^(.*?)\s*(?:\(.*?\))?$`)
)
// The AKS configuration options
type AksOptions struct {
// The namespace used for deploying k8s resources. Defaults to the project name
Namespace string `yaml:"namespace"`
// The relative folder path from the service that contains the k8s deployment manifests. Defaults to 'manifests'
DeploymentPath string `yaml:"deploymentPath"`
// The services ingress configuration options
Ingress AksIngressOptions `yaml:"ingress"`
// The services deployment configuration options
Deployment AksDeploymentOptions `yaml:"deployment"`
// The services service configuration options
Service AksServiceOptions `yaml:"service"`
// The helm configuration options
Helm *helm.Config `yaml:"helm"`
// The kustomize configuration options
Kustomize *kustomize.Config `yaml:"kustomize"`
}
// The AKS ingress options
type AksIngressOptions struct {
Name string `yaml:"name"`
RelativePath string `yaml:"relativePath"`
}
// The AKS deployment options
type AksDeploymentOptions struct {
Name string `yaml:"name"`
}
// The AKS service configuration options
type AksServiceOptions struct {
Name string `yaml:"name"`
}
type aksTarget struct {
env *environment.Environment
envManager environment.Manager
console input.Console
managedClustersService azapi.ManagedClustersService
resourceManager ResourceManager
kubectl *kubectl.Cli
kubeLoginCli *kubelogin.Cli
helmCli *helm.Cli
kustomizeCli *kustomize.Cli
containerHelper *ContainerHelper
featureManager *alpha.FeatureManager
}
// Creates a new instance of the AKS service target
func NewAksTarget(
env *environment.Environment,
envManager environment.Manager,
console input.Console,
managedClustersService azapi.ManagedClustersService,
resourceManager ResourceManager,
kubectlCli *kubectl.Cli,
kubeLoginCli *kubelogin.Cli,
helmCli *helm.Cli,
kustomizeCli *kustomize.Cli,
containerHelper *ContainerHelper,
featureManager *alpha.FeatureManager,
) ServiceTarget {
return &aksTarget{
env: env,
envManager: envManager,
console: console,
managedClustersService: managedClustersService,
resourceManager: resourceManager,
kubectl: kubectlCli,
kubeLoginCli: kubeLoginCli,
helmCli: helmCli,
kustomizeCli: kustomizeCli,
containerHelper: containerHelper,
featureManager: featureManager,
}
}
// Gets the required external tools to support the AKS service
func (t *aksTarget) RequiredExternalTools(ctx context.Context, serviceConfig *ServiceConfig) []tools.ExternalTool {
allTools := []tools.ExternalTool{}
allTools = append(allTools, t.containerHelper.RequiredExternalTools(ctx, serviceConfig)...)
allTools = append(allTools, t.kubectl)
if t.featureManager.IsEnabled(featureHelm) {
allTools = append(allTools, t.helmCli)
}
if t.featureManager.IsEnabled(featureKustomize) {
allTools = append(allTools, t.kustomizeCli)
}
return allTools
}
// Initializes the AKS service target
func (t *aksTarget) Initialize(ctx context.Context, serviceConfig *ServiceConfig) error {
// Ensure that the k8s context has been configured by the time a deploy operation is performed.
// We attach to "postprovision" so that any predeploy or postprovision hooks can take advantage of the configuration
err := serviceConfig.Project.AddHandler(
"postprovision",
func(ctx context.Context, args ProjectLifecycleEventArgs) error {
// Only set the k8s context if we are not in preview mode
previewMode, has := args.Args["preview"]
if !has || !previewMode.(bool) {
return t.setK8sContext(ctx, serviceConfig, "postprovision")
}
return nil
},
)
if err != nil {
return fmt.Errorf("failed adding postprovision handler, %w", err)
}
// Ensure that the k8s context has been configured by the time a deploy operation is performed.
// We attach to "predeploy" so that any predeploy hooks can take advantage of the configuration
err = serviceConfig.AddHandler("predeploy", func(ctx context.Context, args ServiceLifecycleEventArgs) error {
return t.setK8sContext(ctx, serviceConfig, "predeploy")
})
if err != nil {
return fmt.Errorf("failed adding predeploy handler, %w", err)
}
return nil
}
// Prepares and tags the container image from the build output based on the specified service configuration
func (t *aksTarget) Package(
ctx context.Context,
serviceConfig *ServiceConfig,
packageOutput *ServicePackageResult,
progress *async.Progress[ServiceProgress],
) (*ServicePackageResult, error) {
return packageOutput, nil
}
// Deploys service container images to ACR and AKS resources to the AKS cluster
func (t *aksTarget) Deploy(
ctx context.Context,
serviceConfig *ServiceConfig,
packageOutput *ServicePackageResult,
targetResource *environment.TargetResource,
progress *async.Progress[ServiceProgress],
) (*ServiceDeployResult, error) {
if err := t.validateTargetResource(targetResource); err != nil {
return nil, fmt.Errorf("validating target resource: %w", err)
}
if packageOutput == nil {
return nil, errors.New("missing package output")
}
// Only deploy the container image if a package output has been defined
// Empty package details is a valid scenario for any AKS deployment that does not build any containers
// Ex) Helm charts, or other manifests that reference external images
if serviceConfig.Docker.RemoteBuild || packageOutput.Details != nil || packageOutput.PackagePath != "" {
// Login, tag & push container image to ACR
_, err := t.containerHelper.Deploy(ctx, serviceConfig, packageOutput, targetResource, true, progress)
if err != nil {
return nil, err
}
}
// Sync environment
t.kubectl.SetEnv(t.env.Dotenv())
// Deploy k8s resources in the following order:
// 1. Helm
// 2. Kustomize
// 3. Manifests
//
// Users may install a helm chart to setup their cluster with custom resource definitions that their
// custom manifests depend on.
// Users are more likely to either deploy with kustomize or vanilla manifests but they could do both.
deployed := false
// Helm Support
helmDeployed, err := t.deployHelmCharts(ctx, serviceConfig, progress)
if err != nil {
return nil, fmt.Errorf("helm deployment failed: %w", err)
}
deployed = deployed || helmDeployed
// Kustomize Support
kustomizeDeployed, err := t.deployKustomize(ctx, serviceConfig, progress)
if err != nil {
return nil, fmt.Errorf("kustomize deployment failed: %w", err)
}
deployed = deployed || kustomizeDeployed
// Vanilla k8s manifests with minimal templating support
manifestsDeployed, deployment, err := t.deployManifests(ctx, serviceConfig, progress)
if err != nil && !os.IsNotExist(err) {
return nil, err
}
deployed = deployed || manifestsDeployed
if !deployed {
return nil, errors.New("no deployment manifests found")
}
progress.SetProgress(NewServiceProgress("Fetching endpoints for AKS service"))
endpoints, err := t.Endpoints(ctx, serviceConfig, targetResource)
if err != nil {
return nil, err
}
if len(endpoints) > 0 {
// The AKS endpoints contain some additional identifying information
// Regex is used to pull the URL ignoring the additional metadata
// The last endpoint in the array will be the most publicly exposed
matches := endpointRegex.FindStringSubmatch(endpoints[len(endpoints)-1])
if len(matches) > 1 {
t.env.SetServiceProperty(serviceConfig.Name, "ENDPOINT_URL", matches[1])
if err := t.envManager.Save(ctx, t.env); err != nil {
return nil, fmt.Errorf("failed updating environment with endpoint url, %w", err)
}
}
}
return &ServiceDeployResult{
Package: packageOutput,
TargetResourceId: azure.KubernetesServiceRID(
targetResource.SubscriptionId(),
targetResource.ResourceGroupName(),
targetResource.ResourceName(),
),
Kind: AksTarget,
Details: deployment,
Endpoints: endpoints,
}, nil
}
// deployManifests deploys raw or templated yaml manifests to the k8s cluster
func (t *aksTarget) deployManifests(
ctx context.Context,
serviceConfig *ServiceConfig,
task *async.Progress[ServiceProgress],
) (bool, *kubectl.Deployment, error) {
deploymentPath := serviceConfig.K8s.DeploymentPath
if deploymentPath == "" {
deploymentPath = defaultDeploymentPath
}
deploymentPath = filepath.Join(serviceConfig.Path(), deploymentPath)
// Manifests are optional so we will continue if the directory does not exist
if _, err := os.Stat(deploymentPath); os.IsNotExist(err) {
return false, nil, err
}
task.SetProgress(NewServiceProgress("Applying k8s manifests"))
err := t.kubectl.Apply(
ctx,
deploymentPath,
nil,
)
if err != nil {
return false, nil, fmt.Errorf("failed applying kube manifests: %w", err)
}
deploymentName := serviceConfig.K8s.Deployment.Name
if deploymentName == "" {
deploymentName = serviceConfig.Name
}
// It is not a requirement for a AZD deploy to contain a deployment object
// If we don't find any deployment within the namespace we will continue
task.SetProgress(NewServiceProgress("Verifying deployment"))
deployment, err := t.waitForDeployment(ctx, deploymentName)
if err != nil && !errors.Is(err, kubectl.ErrResourceNotFound) {
// We continue to return a true value here since at this point we have successfully applied the manifests
// even through the deployment may not have been found
return true, nil, err
}
return true, deployment, nil
}
// deployKustomize deploys kustomize manifests to the k8s cluster
func (t *aksTarget) deployKustomize(
ctx context.Context,
serviceConfig *ServiceConfig,
task *async.Progress[ServiceProgress],
) (bool, error) {
if serviceConfig.K8s.Kustomize == nil {
return false, nil
}
if !t.featureManager.IsEnabled(featureKustomize) {
return false, fmt.Errorf(
"Kustomize support is not enabled. Run '%s' to enable it.",
alpha.GetEnableCommand(featureKustomize),
)
}
task.SetProgress(NewServiceProgress("Applying k8s manifests with Kustomize"))
overlayPath, err := serviceConfig.K8s.Kustomize.Directory.Envsubst(t.env.Getenv)
if err != nil {
return false, fmt.Errorf("failed to envsubst kustomize directory: %w", err)
}
// When deploying with kustomize we need to specify the full path to the kustomize directory.
// This can either be a base or overlay directory but must contain a kustomization.yaml file
kustomizeDir := filepath.Join(serviceConfig.Project.Path, serviceConfig.RelativePath, overlayPath)
if _, err := os.Stat(kustomizeDir); os.IsNotExist(err) {
return false, fmt.Errorf("kustomize directory '%s' does not exist: %w", kustomizeDir, err)
}
// Kustomize does not have a built in way to specify environment variables
// A common well-known solution is to use the kustomize configMapGenerator within your kustomization.yaml
// and then generate a .env file that can be used to generate config maps
// azd can help here to create an .env file from the map specified within azure.yaml kustomize config section
if len(serviceConfig.K8s.Kustomize.Env) > 0 {
builder := strings.Builder{}
for key, exp := range serviceConfig.K8s.Kustomize.Env {
value, err := exp.Envsubst(t.env.Getenv)
if err != nil {
return false, fmt.Errorf("failed to envsubst kustomize env: %w", err)
}
builder.WriteString(fmt.Sprintf("%s=%s\n", key, value))
}
// We are manually writing the .env file since k8s config maps expect unquoted values
// The godotenv library will quote values when writing the file without an option to disable
envFilePath := filepath.Join(kustomizeDir, ".env")
if err := os.WriteFile(envFilePath, []byte(builder.String()), osutil.PermissionFile); err != nil {
return false, fmt.Errorf("failed to write kustomize .env: %w", err)
}
defer os.Remove(envFilePath)
}
// Another common scenario is to use the kustomize edit commands to modify the kustomization.yaml
// configuration before applying the manifests.
// Common scenarios for this would be for modifying the images or namespace used for the deployment
for _, edit := range serviceConfig.K8s.Kustomize.Edits {
editArgs, err := edit.Envsubst(t.env.Getenv)
if err != nil {
return false, fmt.Errorf("failed to envsubst kustomize edit: %w", err)
}
if err := t.kustomizeCli.
WithCwd(kustomizeDir).
Edit(ctx, strings.Split(editArgs, " ")...); err != nil {
return false, err
}
}
// Finally apply manifests with kustomize using the -k flag
if err := t.kubectl.ApplyWithKustomize(ctx, kustomizeDir, nil); err != nil {
return false, err
}
return true, nil
}
// deployHelmCharts deploys helm charts to the k8s cluster
func (t *aksTarget) deployHelmCharts(
ctx context.Context, serviceConfig *ServiceConfig,
task *async.Progress[ServiceProgress],
) (bool, error) {
if serviceConfig.K8s.Helm == nil {
return false, nil
}
if !t.featureManager.IsEnabled(featureHelm) {
return false, fmt.Errorf("Helm support is not enabled. Run '%s' to enable it.", alpha.GetEnableCommand(featureHelm))
}
for _, repo := range serviceConfig.K8s.Helm.Repositories {
task.SetProgress(NewServiceProgress(fmt.Sprintf("Configuring helm repo: %s", repo.Name)))
if err := t.helmCli.AddRepo(ctx, repo); err != nil {
return false, err
}
if err := t.helmCli.UpdateRepo(ctx, repo.Name); err != nil {
return false, err
}
}
for _, release := range serviceConfig.K8s.Helm.Releases {
if release.Namespace == "" {
release.Namespace = t.getK8sNamespace(serviceConfig)
}
if err := t.ensureNamespace(ctx, release.Namespace); err != nil {
return false, err
}
task.SetProgress(NewServiceProgress(fmt.Sprintf("Installing helm release: %s", release.Name)))
if err := t.helmCli.Upgrade(ctx, release); err != nil {
return false, err
}
task.SetProgress(NewServiceProgress(fmt.Sprintf("Checking helm release status: %s", release.Name)))
err := retry.Do(
ctx,
retry.WithMaxDuration(10*time.Minute, retry.NewConstant(5*time.Second)),
func(ctx context.Context) error {
status, err := t.helmCli.Status(ctx, release)
if err != nil {
return err
}
if status.Info.Status != helm.StatusKindDeployed {
fmt.Printf("Status: %s\n", status.Info.Status)
return retry.RetryableError(
fmt.Errorf("helm release '%s' is not ready, %w", release.Name, err),
)
}
return nil
},
)
if err != nil {
return false, err
}
}
return true, nil
}
// Gets the service endpoints for the AKS service target
func (t *aksTarget) Endpoints(
ctx context.Context,
serviceConfig *ServiceConfig,
targetResource *environment.TargetResource,
) ([]string, error) {
serviceName := serviceConfig.K8s.Service.Name
if serviceName == "" {
serviceName = serviceConfig.Name
}
ingressName := serviceConfig.K8s.Service.Name
if ingressName == "" {
ingressName = serviceConfig.Name
}
// Find endpoints for any matching services
// These endpoints would typically be internal cluster accessible endpoints
serviceEndpoints, err := t.getServiceEndpoints(ctx, serviceName)
if err != nil && !errors.Is(err, kubectl.ErrResourceNotFound) {
return nil, fmt.Errorf("failed retrieving service endpoints, %w", err)
}
// Find endpoints for any matching ingress controllers
// These endpoints would typically be publicly accessible endpoints
ingressEndpoints, err := t.getIngressEndpoints(ctx, serviceConfig, ingressName)
if err != nil && !errors.Is(err, kubectl.ErrResourceNotFound) {
return nil, fmt.Errorf("failed retrieving ingress endpoints, %w", err)
}
endpoints := append(serviceEndpoints, ingressEndpoints...)
return endpoints, nil
}
func (t *aksTarget) validateTargetResource(
targetResource *environment.TargetResource,
) error {
if targetResource.ResourceGroupName() == "" {
return fmt.Errorf("missing resource group name: %s", targetResource.ResourceGroupName())
}
return nil
}
func (t *aksTarget) ensureClusterContext(
ctx context.Context,
serviceConfig *ServiceConfig,
targetResource *environment.TargetResource,
defaultNamespace string,
) (string, error) {
kubeConfigPath := t.env.Getenv(kubectl.KubeConfigEnvVarName)
if kubeConfigPath != "" {
return kubeConfigPath, nil
}
// Login to AKS cluster
clusterName, err := t.resolveClusterName(serviceConfig, targetResource)
if err != nil {
return "", err
}
log.Printf("getting AKS credentials for cluster '%s'\n", clusterName)
clusterCreds, err := t.managedClustersService.GetUserCredentials(
ctx,
targetResource.SubscriptionId(),
targetResource.ResourceGroupName(),
clusterName,
)
if err != nil {
return "", fmt.Errorf(
//nolint:lll
"failed retrieving cluster user credentials. Ensure the current principal has been granted rights to the AKS cluster, %w",
err,
)
}
if len(clusterCreds.Kubeconfigs) == 0 {
return "", fmt.Errorf(
"cluster credentials is empty. Ensure the current principal has been granted rights to the AKS cluster. , %w",
err,
)
}
// The kubeConfig that we care about will also be at position 0
// I don't know if there is a valid use case where this credential results would container multiple configs
kubeConfig, err := kubectl.ParseKubeConfig(ctx, clusterCreds.Kubeconfigs[0].Value)
if err != nil {
return "", fmt.Errorf(
"failed parsing kube config. Ensure your configuration is valid yaml. %w",
err,
)
}
// Set default namespace for the context
// This avoids having to specify the namespace for every kubectl command
kubeConfig.Contexts[0].Context.Namespace = defaultNamespace
kubeConfigManager, err := kubectl.NewKubeConfigManager(t.kubectl)
if err != nil {
return "", err
}
// Create or update the kube config/context for the AKS cluster
kubeConfigPath, err = kubeConfigManager.AddOrUpdateContext(ctx, clusterName, kubeConfig)
if err != nil {
return "", fmt.Errorf("failed adding/updating kube context, %w", err)
}
// Get the provisioned cluster properties to inspect configuration
managedCluster, err := t.managedClustersService.Get(
ctx,
targetResource.SubscriptionId(),
targetResource.ResourceGroupName(),
clusterName,
)
if err != nil {
return "", fmt.Errorf("failed retrieving managed cluster, %w", err)
}
azureRbacEnabled := managedCluster.Properties.AADProfile != nil &&
convert.ToValueWithDefault(managedCluster.Properties.AADProfile.EnableAzureRBAC, false)
localAccountsDisabled := convert.ToValueWithDefault(managedCluster.Properties.DisableLocalAccounts, false)
// If we're connecting to a cluster with RBAC enabled and local accounts disabled
// then we need to convert the kube config to use the exec auth module with azd auth
if azureRbacEnabled || localAccountsDisabled {
convertOptions := &kubelogin.ConvertOptions{
Login: "azd",
KubeConfig: kubeConfigPath,
TenantId: t.env.GetTenantId(),
}
if err := tools.EnsureInstalled(ctx, t.kubeLoginCli); err != nil {
return "", err
}
if err := t.kubeLoginCli.ConvertKubeConfig(ctx, convertOptions); err != nil {
return "", err
}
}
// Merge the cluster config/context into the default kube config
kubeConfigPath, err = kubeConfigManager.MergeConfigs(ctx, "config", clusterName)
if err != nil {
return "", err
}
// Setup the default kube context to use the AKS cluster context
if _, err := t.kubectl.ConfigUseContext(ctx, clusterName, nil); err != nil {
return "", fmt.Errorf(
"failed setting kube context '%s'. Ensure the specified context exists. %w", clusterName,
err,
)
}
return kubeConfigPath, nil
}
// Ensures the k8s namespace exists otherwise creates it
func (t *aksTarget) ensureNamespace(ctx context.Context, namespace string) error {
namespaceResult, err := t.kubectl.CreateNamespace(
ctx,
namespace,
&kubectl.KubeCliFlags{
DryRun: kubectl.DryRunTypeClient,
Output: kubectl.OutputTypeYaml,
},
)
if err != nil {
return fmt.Errorf("failed creating kube namespace: %w", err)
}
_, err = t.kubectl.ApplyWithStdIn(ctx, namespaceResult.Stdout, nil)
if err != nil {
return fmt.Errorf("failed applying kube namespace: %w", err)
}
return nil
}
// Finds a deployment using the specified deploymentNameFilter string
// Waits until the deployment rollout is complete and all replicas are accessible
// Additionally confirms rollout is complete by checking the rollout status
func (t *aksTarget) waitForDeployment(
ctx context.Context,
deploymentNameFilter string,
) (*kubectl.Deployment, error) {
// The deployment can appear like it has succeeded when a previous deployment
// was already in place.
deployment, err := kubectl.WaitForResource(
ctx, t.kubectl, kubectl.ResourceTypeDeployment,
func(deployment *kubectl.Deployment) bool {
return strings.Contains(deployment.Metadata.Name, deploymentNameFilter)
},
func(deployment *kubectl.Deployment) bool {
return deployment.Status.AvailableReplicas == deployment.Spec.Replicas
},
)
if err != nil {
return nil, err
}
// Check the rollout status
// This can be a long operation when the deployment is in a failed state such as an ImagePullBackOff loop
_, err = t.kubectl.RolloutStatus(ctx, deployment.Metadata.Name, nil)
if err != nil {
return nil, err
}
return deployment, nil
}
// Finds an ingress using the specified ingressNameFilter string
// Waits until the ingress LoadBalancer has assigned a valid IP address
func (t *aksTarget) waitForIngress(
ctx context.Context,
ingressNameFilter string,
) (*kubectl.Ingress, error) {
return kubectl.WaitForResource(
ctx, t.kubectl, kubectl.ResourceTypeIngress,
func(ingress *kubectl.Ingress) bool {
return strings.Contains(ingress.Metadata.Name, ingressNameFilter)
},
func(ingress *kubectl.Ingress) bool {
for _, config := range ingress.Status.LoadBalancer.Ingress {
if config.Ip != "" {
return true
}
}
return false
},
)
}
// Finds a service using the specified serviceNameFilter string
// Waits until the service is available
func (t *aksTarget) waitForService(
ctx context.Context,
serviceNameFilter string,
) (*kubectl.Service, error) {
return kubectl.WaitForResource(
ctx, t.kubectl, kubectl.ResourceTypeService,
func(service *kubectl.Service) bool {
return strings.Contains(service.Metadata.Name, serviceNameFilter)
},
func(service *kubectl.Service) bool {
// If the service is not a load balancer it should be immediately available
if service.Spec.Type != kubectl.ServiceTypeLoadBalancer {
return true
}
// Load balancer can take some time to be provision by AKS
var ipAddress string
for _, config := range service.Status.LoadBalancer.Ingress {
if config.Ip != "" {
ipAddress = config.Ip
break
}
}
return ipAddress != ""
},
)
}
// Retrieve any service endpoints for the specified serviceNameFilter
// Supports service types for LoadBalancer and ClusterIP
func (t *aksTarget) getServiceEndpoints(
ctx context.Context,
serviceNameFilter string,
) ([]string, error) {
service, err := t.waitForService(ctx, serviceNameFilter)
if err != nil {
return nil, err
}
var endpoints []string
if service.Spec.Type == kubectl.ServiceTypeLoadBalancer {
for _, resource := range service.Status.LoadBalancer.Ingress {
endpoints = append(
endpoints,
fmt.Sprintf("http://%s (Service: %s, Type: LoadBalancer)", resource.Ip, service.Metadata.Name),
)
}
} else if service.Spec.Type == kubectl.ServiceTypeClusterIp {
for index, ip := range service.Spec.ClusterIps {
endpoints = append(
endpoints,
fmt.Sprintf("http://%s:%d (Service: %s, Type: ClusterIP)",
ip,
service.Spec.Ports[index].Port,
service.Metadata.Name,
),
)
}
}
return endpoints, nil
}
// Retrieve any ingress endpoints for the specified serviceNameFilter
// Supports service types for LoadBalancer, supports Hosts and/or IP address
func (t *aksTarget) getIngressEndpoints(
ctx context.Context,
serviceConfig *ServiceConfig,
resourceFilter string,
) ([]string, error) {
ingress, err := t.waitForIngress(ctx, resourceFilter)
if err != nil {
return nil, err
}
var endpoints []string
var protocol string
if len(ingress.Spec.Tls) == 0 {
protocol = "http"
} else {
protocol = "https"
}
for index, resource := range ingress.Status.LoadBalancer.Ingress {
var baseUrl string
if ingress.Spec.Rules[index].Host == nil {
baseUrl = fmt.Sprintf("%s://%s", protocol, resource.Ip)
} else {
baseUrl = fmt.Sprintf("%s://%s", protocol, *ingress.Spec.Rules[index].Host)
}
endpointUrl, err := url.JoinPath(baseUrl, serviceConfig.K8s.Ingress.RelativePath)
if err != nil {
return nil, fmt.Errorf("failed constructing service endpoints, %w", err)
}
endpoints = append(endpoints, fmt.Sprintf("%s (Ingress, Type: LoadBalancer)", endpointUrl))
}
return endpoints, nil
}
func (t *aksTarget) getK8sNamespace(serviceConfig *ServiceConfig) string {
namespace := serviceConfig.K8s.Namespace
if namespace == "" {
namespace = serviceConfig.Project.Name
}
return namespace
}
func (t *aksTarget) setK8sContext(ctx context.Context, serviceConfig *ServiceConfig, eventName ext.Event) error {
t.kubectl.SetEnv(t.env.Dotenv())
hasCustomKubeConfig := false
// If a KUBECONFIG env var is set, use it.
kubeConfigPath := t.env.Getenv(kubectl.KubeConfigEnvVarName)
if kubeConfigPath != "" {
t.kubectl.SetKubeConfig(kubeConfigPath)
hasCustomKubeConfig = true
}
targetResource, err := t.resourceManager.GetTargetResource(ctx, t.env.GetSubscriptionId(), serviceConfig)
if err != nil {
return err
}
defaultNamespace := t.getK8sNamespace(serviceConfig)
_, err = t.ensureClusterContext(ctx, serviceConfig, targetResource, defaultNamespace)
if err != nil {
return err
}
err = t.ensureNamespace(ctx, defaultNamespace)
if err != nil {
return err
}
// Display message to the user when we detect they are using a non-default KUBECONFIG configuration
// In standard AZD AKS deployment users should not typically need to set a custom KUBECONFIG
if hasCustomKubeConfig && eventName == "predeploy" {
t.console.Message(ctx, output.WithWarningFormat("Using KUBECONFIG @ %s\n", kubeConfigPath))
}
return nil
}
// resolveClusterName attempts to resolve the cluster name from the following sources:
// 1. The 'AZD_AKS_CLUSTER' environment variable
// 2. The 'resourceName' property in the azure.yaml (Can use expandable string as well)
// 3. The 'resourceName' property passed the target resource
func (t *aksTarget) resolveClusterName(
serviceConfig *ServiceConfig,
targetResource *environment.TargetResource,
) (string, error) {
// Resolve cluster name
clusterName, found := t.env.LookupEnv(environment.AksClusterEnvVarName)
if !found {
log.Printf("'%s' environment variable not found\n", environment.AksClusterEnvVarName)
}
if clusterName == "" {
yamlClusterName, err := serviceConfig.ResourceName.Envsubst(t.env.Getenv)
if err != nil {
log.Println("failed resolving cluster name from `resourceName` in azure.yaml", err)
}
clusterName = yamlClusterName
}
if clusterName == "" {
clusterName = targetResource.ResourceName()
}
if clusterName == "" {
return "", fmt.Errorf(
// nolint:lll
"could not determine AKS cluster, ensure 'resourceName' is set in your azure.yaml or '%s' environment variable has been set.",
environment.AksClusterEnvVarName,
)
}
return clusterName, nil
}