diff --git a/pkg/webhook/constants/constants.go b/pkg/webhook/constants/constants.go index b4a9214b33d..a9ea9d460ac 100644 --- a/pkg/webhook/constants/constants.go +++ b/pkg/webhook/constants/constants.go @@ -28,4 +28,10 @@ const ( LabeledAction = "labeled" // DocsLabel kicks off the controller when added to a PR DocsLabel = "docs-modifications" + + // Namespace is the namespace deployments and services will be created in + Namespace = "default" + + // HugoPort is the port that hugo defaults to + HugoPort = 1313 ) diff --git a/pkg/webhook/kubernetes/service.go b/pkg/webhook/kubernetes/service.go new file mode 100644 index 00000000000..1c8379e5692 --- /dev/null +++ b/pkg/webhook/kubernetes/service.go @@ -0,0 +1,90 @@ +/* +Copyright 2018 The Skaffold Authors + +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 kubernetes + +import ( + "fmt" + "time" + + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes" + "github.com/GoogleContainerTools/skaffold/pkg/webhook/constants" + "github.com/GoogleContainerTools/skaffold/pkg/webhook/labels" + "github.com/google/go-github/github" + "github.com/pkg/errors" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" +) + +// CreateService creates a service for the deployment to bind to +// and returns the external IP of the service +func CreateService(pr *github.PullRequestEvent) (*v1.Service, error) { + clientset, err := kubernetes.GetClientset() + if err != nil { + return nil, errors.Wrap(err, "getting clientset") + } + l := labels.GenerateLabelsFromPR(pr.GetNumber()) + + key, val := labels.RetrieveLabel(pr.GetNumber()) + selector := map[string]string{key: val} + + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName(pr.GetNumber()), + Labels: l, + }, + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeLoadBalancer, + Ports: []v1.ServicePort{ + { + Port: constants.HugoPort, + }, + }, + Selector: selector, + }, + } + return clientset.CoreV1().Services(constants.Namespace).Create(svc) +} + +// GetExternalIP polls the service until an external IP is available and returns it +func GetExternalIP(s *v1.Service) (string, error) { + var ip string + err := wait.PollImmediate(time.Second*5, time.Minute*5, func() (bool, error) { + svc, err := getService(s) + if err != nil { + return false, nil + } + if len(svc.Status.LoadBalancer.Ingress) > 0 { + ip = svc.Status.LoadBalancer.Ingress[0].IP + return true, nil + } + return false, nil + }) + return ip, err +} + +func serviceName(prNumber int) string { + return fmt.Sprintf("docs-controller-svc-%d", prNumber) +} + +func getService(svc *v1.Service) (*v1.Service, error) { + clientset, err := kubernetes.GetClientset() + if err != nil { + return nil, errors.Wrap(err, "getting clientset") + } + return clientset.CoreV1().Services(svc.Namespace).Get(svc.Name, metav1.GetOptions{}) +} diff --git a/pkg/webhook/labels/labels.go b/pkg/webhook/labels/labels.go index 731a71a9781..f55ef40b4df 100644 --- a/pkg/webhook/labels/labels.go +++ b/pkg/webhook/labels/labels.go @@ -27,19 +27,19 @@ import ( func GenerateLabelsFromPR(prNumber int) map[string]string { m := map[string]string{} m["docs-controller-deployment"] = "true" - k, v := retrieveLabel(prNumber) + k, v := RetrieveLabel(prNumber) m[k] = v return m } // Selector returns the label associated with the pull request func Selector(prNumber int) string { - k, v := retrieveLabel(prNumber) + k, v := RetrieveLabel(prNumber) return fmt.Sprintf("%s=%s", k, v) } -// retrieveLabel returns the key and value for a label associated with this PR number -func retrieveLabel(prNumber int) (string, string) { +// RetrieveLabel returns the key and value for a label associated with this PR number +func RetrieveLabel(prNumber int) (string, string) { value := fmt.Sprintf("docs-controller-deployment-%d", prNumber) return "deployment", value } diff --git a/webhook/webhook.go b/webhook/webhook.go index 26dc8c9fc69..19334d2402d 100644 --- a/webhook/webhook.go +++ b/webhook/webhook.go @@ -23,6 +23,7 @@ import ( "github.com/GoogleContainerTools/skaffold/pkg/webhook/kubernetes" "github.com/GoogleContainerTools/skaffold/pkg/webhook/labels" + "github.com/pkg/errors" "github.com/GoogleContainerTools/skaffold/pkg/webhook/constants" "github.com/google/go-github/github" @@ -66,18 +67,32 @@ func handlePullRequestEvent(event *github.PullRequestEvent) error { return nil } + prNumber := event.GetNumber() + if event.PullRequest.GetMerged() || event.PullRequest.ClosedAt == nil { - log.Printf("Pull request %d is either merged or closed, skipping docs deployment", event.GetNumber()) + log.Printf("Pull request %d is either merged or closed, skipping docs deployment", prNumber) return nil } if !labels.DocsLabelExists(event.GetPullRequest().Labels) { - log.Printf("Label %s not found on PR %d", constants.DocsLabel, event.GetNumber()) + log.Printf("Label %s not found on PR %d", constants.DocsLabel, prNumber) return nil } - log.Printf("Label %s found on PR %d", constants.DocsLabel, event.GetNumber()) + log.Printf("Label %s found on PR %d, creating service", constants.DocsLabel, prNumber) + + svc, err := kubernetes.CreateService(event) + if err != nil { + return errors.Wrap(err, "creating service") + } + + ip, err := kubernetes.GetExternalIP(svc) + if err != nil { + return errors.Wrap(err, "getting external IP") + } + + log.Printf("External IP %s ready for PR %d", ip, prNumber) - // TODO: priyawadhwa@ to add logic for creating a service and deployment here + // TODO: priyawadhwa@ to add logic for creating a deployment mapping to a service here return nil }