github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cmd/step/verify/step_verify_ingress.go (about)

     1  package verify
     2  
     3  import (
     4  	"fmt"
     5  	"net/mail"
     6  	"os"
     7  	"time"
     8  
     9  	"github.com/olli-ai/jx/v2/pkg/cmd/opts/step"
    10  
    11  	"github.com/olli-ai/jx/v2/pkg/cloud/gke"
    12  	"github.com/olli-ai/jx/v2/pkg/cloud/gke/externaldns"
    13  	"github.com/olli-ai/jx/v2/pkg/config"
    14  	"github.com/olli-ai/jx/v2/pkg/kube"
    15  	"github.com/olli-ai/jx/v2/pkg/util"
    16  
    17  	"github.com/jenkins-x/jx-logging/pkg/log"
    18  	"github.com/olli-ai/jx/v2/pkg/cloud"
    19  	"github.com/olli-ai/jx/v2/pkg/cmd/helper"
    20  	"github.com/olli-ai/jx/v2/pkg/cmd/opts"
    21  	"github.com/olli-ai/jx/v2/pkg/cmd/templates"
    22  	"github.com/pkg/errors"
    23  	"github.com/spf13/cobra"
    24  	pipelineapi "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/client-go/kubernetes"
    27  )
    28  
    29  var (
    30  	verifyIngressLong = templates.LongDesc(`
    31  		Verifies the ingress configuration defaulting the ingress domain if necessary
    32  `)
    33  
    34  	verifyIngressExample = templates.Examples(`
    35  		# populate the ingress domain if not using a configured 'ingress.domain' setting
    36  		jx step verify ingress
    37  
    38  			`)
    39  )
    40  
    41  // StepVerifyIngressOptions contains the command line flags
    42  type StepVerifyIngressOptions struct {
    43  	step.StepOptions
    44  
    45  	Dir              string
    46  	Namespace        string
    47  	Provider         string
    48  	IngressNamespace string
    49  	IngressService   string
    50  	ExternalIP       string
    51  	LazyCreate       bool
    52  	LazyCreateFlag   string
    53  }
    54  
    55  // StepVerifyIngressResults stores the generated results
    56  type StepVerifyIngressResults struct {
    57  	Pipeline    *pipelineapi.Pipeline
    58  	Task        *pipelineapi.Task
    59  	PipelineRun *pipelineapi.PipelineRun
    60  }
    61  
    62  // NewCmdStepVerifyIngress Creates a new Command object
    63  func NewCmdStepVerifyIngress(commonOpts *opts.CommonOptions) *cobra.Command {
    64  	options := &StepVerifyIngressOptions{
    65  		StepOptions: step.StepOptions{
    66  			CommonOptions: commonOpts,
    67  		},
    68  	}
    69  
    70  	cmd := &cobra.Command{
    71  		Use:     "ingress",
    72  		Short:   "Verifies the ingress configuration defaulting the ingress domain if necessary",
    73  		Long:    verifyIngressLong,
    74  		Example: verifyIngressExample,
    75  		Run: func(cmd *cobra.Command, args []string) {
    76  			options.Cmd = cmd
    77  			options.Args = args
    78  			err := options.Run()
    79  			helper.CheckErr(err)
    80  		},
    81  	}
    82  
    83  	cmd.Flags().StringVarP(&options.Dir, "dir", "d", ".", "the directory to look for the values.yaml file")
    84  	cmd.Flags().StringVarP(&options.Namespace, "namespace", "n", "", "the namespace to install into. Defaults to $DEPLOY_NAMESPACE if not")
    85  
    86  	cmd.Flags().StringVarP(&options.IngressNamespace, "ingress-namespace", "", opts.DefaultIngressNamesapce, "The namespace for the Ingress controller")
    87  	cmd.Flags().StringVarP(&options.IngressService, "ingress-service", "", opts.DefaultIngressServiceName, "The name of the Ingress controller Service")
    88  	cmd.Flags().StringVarP(&options.ExternalIP, "external-ip", "", "", "The external IP used to access ingress endpoints from outside the Kubernetes cluster. For bare metal on premise clusters this is often the IP of the Kubernetes master. For cloud installations this is often the external IP of the ingress LoadBalancer.")
    89  	cmd.Flags().StringVarP(&options.Provider, "provider", "", "", "Cloud service providing the Kubernetes cluster.  Supported providers: "+cloud.KubernetesProviderOptions())
    90  	cmd.Flags().StringVarP(&options.LazyCreateFlag, "lazy-create", "", "", fmt.Sprintf("Specify true/false as to whether to lazily create missing resources. If not specified it is enabled if Terraform is not specified in the %s file", config.RequirementsConfigFileName))
    91  	return cmd
    92  }
    93  
    94  // Run implements this command
    95  func (o *StepVerifyIngressOptions) Run() error {
    96  	var err error
    97  	if o.Dir == "" {
    98  		o.Dir, err = os.Getwd()
    99  		if err != nil {
   100  			return err
   101  		}
   102  	}
   103  
   104  	info := util.ColorInfo
   105  	ns := o.Namespace
   106  	if ns == "" {
   107  		ns = os.Getenv("DEPLOY_NAMESPACE")
   108  	}
   109  	if ns != "" {
   110  		if ns == "" {
   111  			return fmt.Errorf("no default namespace found")
   112  		}
   113  	}
   114  	requirements, requirementsFileName, err := config.LoadRequirementsConfig(o.Dir, config.DefaultFailOnValidationError)
   115  	if err != nil {
   116  		return errors.Wrapf(err, "failed to load Jenkins X requirements")
   117  	}
   118  
   119  	o.LazyCreate, err = requirements.IsLazyCreateSecrets(o.LazyCreateFlag)
   120  	if err != nil {
   121  		return errors.Wrapf(err, "failed to see if lazy create flag is set %s", o.LazyCreateFlag)
   122  	}
   123  
   124  	if requirements.Cluster.Provider == "" {
   125  		log.Logger().Warnf("No provider configured\n")
   126  	}
   127  
   128  	if requirements.Ingress.Domain == "" {
   129  		err = o.discoverIngressDomain(requirements, requirementsFileName)
   130  		if err != nil {
   131  			return errors.Wrapf(err, "failed to discover the Ingress domain")
   132  		}
   133  	}
   134  
   135  	// if we're using GKE and folks have provided a domain, i.e. we're  not using the Jenkins X default nip.io
   136  	if requirements.Ingress.Domain != "" && !requirements.Ingress.IsAutoDNSDomain() && requirements.Cluster.Provider == cloud.GKE {
   137  		// then it may be a good idea to enable external dns and TLS
   138  		if !requirements.Ingress.ExternalDNS {
   139  			log.Logger().Info("using a custom domain and GKE, you can enable external dns and TLS")
   140  		} else if !requirements.Ingress.TLS.Enabled {
   141  			log.Logger().Info("using GKE with external dns, you can also now enable TLS")
   142  		}
   143  
   144  		if requirements.Ingress.ExternalDNS {
   145  			log.Logger().Infof("validating the external-dns secret in namespace %s\n", info(ns))
   146  
   147  			kubeClient, err := o.KubeClient()
   148  			if err != nil {
   149  				return errors.Wrap(err, "creating kubernetes client")
   150  			}
   151  
   152  			cloudDNSSecretName := requirements.Ingress.CloudDNSSecretName
   153  			if cloudDNSSecretName == "" {
   154  				cloudDNSSecretName = gke.GcpServiceAccountSecretName(kube.DefaultExternalDNSReleaseName)
   155  				requirements.Ingress.CloudDNSSecretName = cloudDNSSecretName
   156  			}
   157  
   158  			err = kube.ValidateSecret(kubeClient, cloudDNSSecretName, externaldns.ServiceAccountSecretKey, ns)
   159  			if err != nil {
   160  				if o.LazyCreate {
   161  					log.Logger().Infof("attempting to lazily create the external-dns secret %s\n", info(ns))
   162  
   163  					_, err = externaldns.CreateExternalDNSGCPServiceAccount(o.GCloud(), kubeClient, kube.DefaultExternalDNSReleaseName, ns,
   164  						requirements.Cluster.ClusterName, requirements.Cluster.ProjectID)
   165  					if err != nil {
   166  						return errors.Wrap(err, "creating the ExternalDNS GCP Service Account")
   167  					}
   168  					// lets rerun the verify step to ensure its all sorted now
   169  					err = kube.ValidateSecret(kubeClient, cloudDNSSecretName, externaldns.ServiceAccountSecretKey, ns)
   170  				}
   171  			}
   172  			if err != nil {
   173  				return errors.Wrap(err, "validating external-dns secret")
   174  			}
   175  
   176  			err = o.GCloud().EnableAPIs(requirements.Cluster.ProjectID, "dns")
   177  			if err != nil {
   178  				return errors.Wrap(err, "unable to enable 'dns' api")
   179  			}
   180  		}
   181  	}
   182  
   183  	// TLS uses cert-manager to ask LetsEncrypt for a signed certificate
   184  	if requirements.Ingress.TLS.Enabled {
   185  		if requirements.Cluster.Provider != cloud.GKE {
   186  			log.Logger().Warnf("Note that we have only tested TLS support on Google Container Engine with external-dns so far. This may not work!")
   187  		}
   188  
   189  		if requirements.Ingress.IsAutoDNSDomain() {
   190  			return fmt.Errorf("TLS is not supported with automated domains like %s, you will need to use a real domain you own", requirements.Ingress.Domain)
   191  		}
   192  		_, err = mail.ParseAddress(requirements.Ingress.TLS.Email)
   193  		if err != nil {
   194  			return errors.Wrap(err, "You must provide a valid email address to enable TLS so you can receive notifications from LetsEncrypt about your certificates")
   195  		}
   196  	}
   197  
   198  	return requirements.SaveConfig(requirementsFileName)
   199  }
   200  
   201  func (o *StepVerifyIngressOptions) discoverIngressDomain(requirements *config.RequirementsConfig, requirementsFileName string) error {
   202  	if requirements.Ingress.IgnoreLoadBalancer {
   203  		log.Logger().Infof("ignoring the load balancer to detect a public ingress domain")
   204  		return nil
   205  	}
   206  	client, err := o.KubeClient()
   207  	var domain string
   208  	if err != nil {
   209  		return errors.Wrap(err, "getting the kubernetes client")
   210  	}
   211  
   212  	if requirements.Ingress.Domain != "" {
   213  		return nil
   214  	}
   215  
   216  	if o.Provider == "" {
   217  		o.Provider = requirements.Cluster.Provider
   218  		if o.Provider == "" {
   219  			log.Logger().Warnf("No provider configured\n")
   220  		}
   221  	}
   222  	domain, err = o.GetDomain(client, "",
   223  		o.Provider,
   224  		o.IngressNamespace,
   225  		o.IngressService,
   226  		o.ExternalIP)
   227  	if err != nil {
   228  		return errors.Wrapf(err, "getting a domain for ingress service %s/%s", o.IngressNamespace, o.IngressService)
   229  	}
   230  	if domain == "" {
   231  		hasHost, err := o.waitForIngressControllerHost(client, o.IngressNamespace, o.IngressService)
   232  		if err != nil {
   233  			return errors.Wrapf(err, "getting a domain for ingress service %s/%s", o.IngressNamespace, o.IngressService)
   234  		}
   235  		if hasHost {
   236  			domain, err = o.GetDomain(client, "",
   237  				o.Provider,
   238  				o.IngressNamespace,
   239  				o.IngressService,
   240  				o.ExternalIP)
   241  			if err != nil {
   242  				return errors.Wrapf(err, "getting a domain for ingress service %s/%s", o.IngressNamespace, o.IngressService)
   243  			}
   244  		} else {
   245  			log.Logger().Warnf("could not find host for  ingress service %s/%s\n", o.IngressNamespace, o.IngressService)
   246  		}
   247  	}
   248  
   249  	if domain == "" {
   250  		return fmt.Errorf("failed to discover domain for ingress service %s/%s", o.IngressNamespace, o.IngressService)
   251  	}
   252  	requirements.Ingress.Domain = domain
   253  	err = requirements.SaveConfig(requirementsFileName)
   254  	if err != nil {
   255  		return errors.Wrapf(err, "failed to save changes to file: %s", requirementsFileName)
   256  	}
   257  	log.Logger().Infof("defaulting the domain to %s and modified %s\n", util.ColorInfo(domain), util.ColorInfo(requirementsFileName))
   258  	return nil
   259  }
   260  
   261  func (o *StepVerifyIngressOptions) waitForIngressControllerHost(kubeClient kubernetes.Interface, ns, serviceName string) (bool, error) {
   262  	loggedWait := false
   263  	serviceInterface := kubeClient.CoreV1().Services(ns)
   264  
   265  	if serviceName == "" || ns == "" {
   266  		return false, nil
   267  	}
   268  	_, err := serviceInterface.Get(serviceName, metav1.GetOptions{})
   269  	if err != nil {
   270  		return false, err
   271  	}
   272  
   273  	fn := func() (bool, error) {
   274  		svc, err := serviceInterface.Get(serviceName, metav1.GetOptions{})
   275  		if err != nil {
   276  			return false, err
   277  		}
   278  
   279  		// lets get the ingress service status
   280  		for _, lb := range svc.Status.LoadBalancer.Ingress {
   281  			if lb.Hostname != "" || lb.IP != "" {
   282  				return true, nil
   283  			}
   284  		}
   285  
   286  		if !loggedWait {
   287  			loggedWait = true
   288  			log.Logger().Infof("waiting for external Host on the ingress service %s in namespace %s ...", serviceName, ns)
   289  		}
   290  		return false, nil
   291  	}
   292  	err = o.RetryUntilTrueOrTimeout(time.Minute*5, time.Second*3, fn)
   293  	if err != nil {
   294  		return false, err
   295  	}
   296  	return true, nil
   297  }