Explorar o código

Merge branch 'belanger/por-83-usage-enforcement' of https://github.com/porter-dev/porter into belanger/por-83-usage-enforcement

mergin
Alexander Belanger %!s(int64=4) %!d(string=hai) anos
pai
achega
8382b3d6c1

+ 1 - 0
.github/workflows/production.yaml

@@ -30,6 +30,7 @@ jobs:
           cat >./dashboard/.env <<EOL
           NODE_ENV=production
           API_SERVER=dashboard.getporter.dev
+          COHERE_API_KEY=${{secrets.COHERE_API_KEY}}
           DISCORD_KEY=${{secrets.DISCORD_KEY}}
           DISCORD_CID=${{secrets.DISCORD_CID}}
           FEEDBACK_ENDPOINT=${{secrets.FEEDBACK_ENDPOINT}}

+ 23 - 1
.github/workflows/release.yaml

@@ -113,7 +113,7 @@ jobs:
           retention-days: 1
   notarize:
     name: Notarize Darwin binaries
-    runs-on: macos-latest
+    runs-on: macos-11
     needs: build
     steps:
       - name: Get tag name
@@ -314,6 +314,28 @@ jobs:
           asset_path: ./release/windows/porter_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
           asset_name: porter_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
           asset_content_type: application/zip
+      - name: Upload Windows Server Release Asset
+        id: upload-windows-server-release-asset
+        uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          GITHUB_TAG: ${{ github.ref }}
+        with:
+          upload_url: ${{ steps.create_release.outputs.upload_url }}
+          asset_path: ./release/windows/portersvr_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
+          asset_name: portersvr_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
+          asset_content_type: application/zip
+      - name: Upload Windows Docker Credential Release Asset
+        id: upload-windows-docker-cred-release-asset
+        uses: actions/upload-release-asset@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          GITHUB_TAG: ${{ github.ref }}
+        with:
+          upload_url: ${{ steps.create_release.outputs.upload_url }}
+          asset_path: ./release/windows/docker-credential-porter_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
+          asset_name: docker-credential-porter_${{steps.tag_name.outputs.tag}}_Windows_x86_64.zip
+          asset_content_type: application/zip
       - name: Upload Static Release Asset
         id: upload-static-release-asset
         uses: actions/upload-release-asset@v1

+ 1 - 1
api/server/handlers/release/create_subdomain.go

@@ -67,7 +67,7 @@ func (c *CreateSubdomainHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 
 	_record := domain.DNSRecord(*record)
 
-	err = _record.CreateDomain(c.Config().IngressAgent.Clientset)
+	err = _record.CreateDomain(c.Config().PowerDNSClient)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 32 - 0
api/server/handlers/release/get.go

@@ -3,12 +3,14 @@ package release
 import (
 	"net/http"
 
+	semver "github.com/Masterminds/semver/v3"
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/helm/loader"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/templater/parser"
 	"gorm.io/gorm"
@@ -53,6 +55,36 @@ func (c *ReleaseGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	} else if err != gorm.ErrRecordNotFound {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
+	} else {
+		res.PorterRelease = &types.PorterRelease{}
+	}
+
+	// detect if Porter application chart and attempt to get the latest version
+	// from chart repo
+	cache := c.Config().URLCache
+	chartRepoURL, foundFirst := cache.GetURL(helmRelease.Chart.Metadata.Name)
+
+	if !foundFirst {
+		cache.Update()
+
+		chartRepoURL, _ = cache.GetURL(helmRelease.Chart.Metadata.Name)
+	}
+
+	if chartRepoURL != "" {
+		repoIndex, err := loader.LoadRepoIndexPublic(chartRepoURL)
+
+		if err == nil {
+			porterChart := loader.FindPorterChartInIndexList(repoIndex, res.Chart.Metadata.Name)
+			res.LatestVersion = res.Chart.Metadata.Version
+
+			// set latest version to the greater of porterChart.Versions and res.Chart.Metadata.Version
+			porterChartVersion, porterChartErr := semver.NewVersion(porterChart.Versions[0])
+			currChartVersion, currChartErr := semver.NewVersion(res.Chart.Metadata.Version)
+
+			if currChartErr == nil && porterChartErr == nil && porterChartVersion.GreaterThan(currChartVersion) {
+				res.LatestVersion = porterChart.Versions[0]
+			}
+		}
 	}
 
 	// look for the form using the dynamic client

+ 73 - 2
api/server/handlers/release/get_all_pods.go

@@ -1,6 +1,7 @@
 package release
 
 import (
+	"fmt"
 	"net/http"
 	"strings"
 
@@ -11,6 +12,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/helm/grapher"
+	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
 	"helm.sh/helm/v3/pkg/release"
 	v1 "k8s.io/api/core/v1"
@@ -58,8 +60,38 @@ func (c *GetAllPodsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 		selectors := make([]string, 0)
 
-		for key, val := range selector.MatchLabels {
-			selectors = append(selectors, key+"="+val)
+		if strings.ToLower(controller.Kind) == "cronjob" {
+			// in the case of cronjobs, getting the pod is non-arbitrary. We only get the pod
+			// declared by the manifest, which will have a certain revision attached. But the
+			// label on the pod is the job name, not the cronjob name. So we first find the
+			// list of jobs run by this cronjob, and then get the pods attached to that job.
+			jobLabels := make([]kubernetes.Label, 0)
+
+			for key, val := range selector.MatchLabels {
+				jobLabels = append(jobLabels, kubernetes.Label{
+					Key: key,
+					Val: val,
+				})
+			}
+
+			jobPods, err := getPodsForJobs(agent, helmRelease.Namespace, jobLabels)
+
+			if err != nil {
+				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+				return
+			}
+
+			pods = append(pods, jobPods...)
+
+			continue
+		} else if strings.ToLower(controller.Kind) == "job" {
+			// in the case of jobs as the controller, we simply find the job matching the
+			// pod name.
+			selectors = append(selectors, "job-name="+controller.Name)
+		} else {
+			for key, val := range selector.MatchLabels {
+				selectors = append(selectors, key+"="+val)
+			}
 		}
 
 		podList, err := agent.GetPodsByLabel(strings.Join(selectors, ","), helmRelease.Namespace)
@@ -72,5 +104,44 @@ func (c *GetAllPodsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		pods = append(pods, podList.Items...)
 	}
 
+	// we also check for jobs attached to this release
+	labels := getJobLabels(helmRelease)
+
+	labels = append(labels, kubernetes.Label{
+		Key: "helm.sh/revision",
+		Val: fmt.Sprintf("%d", helmRelease.Version),
+	})
+
+	jobPods, err := getPodsForJobs(agent, helmRelease.Namespace, labels)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	pods = append(pods, jobPods...)
+
 	c.WriteResult(w, r, pods)
 }
+
+func getPodsForJobs(agent *kubernetes.Agent, namespace string, labels []kubernetes.Label) ([]v1.Pod, error) {
+	pods := make([]v1.Pod, 0)
+
+	jobs, err := agent.ListJobsByLabel(namespace, labels...)
+
+	if err != nil {
+		return nil, err
+	}
+
+	for _, job := range jobs {
+		podList, err := agent.GetPodsByLabel("job-name="+job.Name, namespace)
+
+		if err != nil {
+			return nil, err
+		}
+
+		pods = append(pods, podList.Items...)
+	}
+
+	return pods, nil
+}

+ 17 - 1
api/server/handlers/release/get_controllers.go

@@ -114,7 +114,23 @@ func getController(controller grapher.Object, agent *kubernetes.Agent) (rc inter
 			return nil, nil, err
 		}
 
-		return obj, obj.Spec.JobTemplate.Spec.Selector, nil
+		res := &metav1.LabelSelector{
+			MatchLabels: make(map[string]string),
+		}
+
+		for key, val := range obj.Spec.JobTemplate.Labels {
+			res.MatchLabels[key] = val
+		}
+
+		return obj, res, nil
+	case "job":
+		obj, err := agent.GetJob(controller)
+
+		if err != nil {
+			return nil, nil, err
+		}
+
+		return obj, obj.Spec.Selector, nil
 	}
 
 	return nil, nil, fmt.Errorf("not a valid controller")

+ 4 - 0
api/server/handlers/template/get.go

@@ -45,6 +45,10 @@ func (t *TemplateGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		version = ""
 	}
 
+	if request.RepoURL == "" {
+		request.RepoURL = t.Config().ServerConf.DefaultApplicationHelmRepoURL
+	}
+
 	chart, err := loader.LoadChartPublic(request.RepoURL, name, version)
 
 	if err != nil {

+ 4 - 4
api/server/shared/config/config.go

@@ -9,6 +9,7 @@ import (
 	"github.com/porter-dev/porter/internal/auth/token"
 	"github.com/porter-dev/porter/internal/billing"
 	"github.com/porter-dev/porter/internal/helm/urlcache"
+	"github.com/porter-dev/porter/internal/integrations/powerdns"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/logger"
 	"github.com/porter-dev/porter/internal/notifier"
@@ -77,10 +78,6 @@ type Config struct {
 	// jobs
 	ProvisionerAgent *kubernetes.Agent
 
-	// IngressAgent is the kubernetes client responsible for creating new ingress
-	// resources
-	IngressAgent *kubernetes.Agent
-
 	// DB is the gorm DB instance
 	DB *gorm.DB
 
@@ -92,6 +89,9 @@ type Config struct {
 
 	// WhitelistedUsers do not count toward usage limits
 	WhitelistedUsers map[uint]uint
+
+  // PowerDNSClient is a client for PowerDNS, if the Porter instance supports vanity URLs
+	PowerDNSClient *powerdns.Client
 }
 
 type ConfigLoader interface {

+ 4 - 0
api/server/shared/config/env/envconfs.go

@@ -59,6 +59,10 @@ type ServerConf struct {
 	ProvisionerImagePullSecret string `env:"PROV_IMAGE_PULL_SECRET"`
 	SegmentClientKey           string `env:"SEGMENT_CLIENT_KEY"`
 
+	// PowerDNS client API key and the host of the PowerDNS API server
+	PowerDNSAPIServerURL string `env:"POWER_DNS_API_SERVER_URL"`
+	PowerDNSAPIKey       string `env:"POWER_DNS_API_KEY"`
+
 	// Email for an admin user. On a self-hosted instance of Porter, the
 	// admin user is the only user that can log in and register. After the admin
 	// user has logged in, registration is turned off.

+ 4 - 25
api/server/shared/config/loader/loader.go

@@ -17,6 +17,7 @@ import (
 	"github.com/porter-dev/porter/internal/auth/token"
 	"github.com/porter-dev/porter/internal/billing"
 	"github.com/porter-dev/porter/internal/helm/urlcache"
+	"github.com/porter-dev/porter/internal/integrations/powerdns"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/kubernetes/local"
 	"github.com/porter-dev/porter/internal/notifier"
@@ -211,16 +212,12 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 		res.Metadata.Provisioning = true
 	}
 
-	ingressAgent, err := getIngressAgent(sc)
+	res.AnalyticsClient = analytics.InitializeAnalyticsSegmentClient(sc.SegmentClientKey, res.Logger)
 
-	if err != nil {
-		return nil, err
+	if sc.PowerDNSAPIKey != "" && sc.PowerDNSAPIServerURL != "" {
+		res.PowerDNSClient = powerdns.NewClient(sc.PowerDNSAPIServerURL, sc.PowerDNSAPIKey, sc.AppRootDomain)
 	}
 
-	res.IngressAgent = ingressAgent
-
-	res.AnalyticsClient = analytics.InitializeAnalyticsSegmentClient(sc.SegmentClientKey, res.Logger)
-
 	return res, nil
 }
 
@@ -241,21 +238,3 @@ func getProvisionerAgent(sc *env.ServerConf) (*kubernetes.Agent, error) {
 
 	return agent, nil
 }
-
-func getIngressAgent(sc *env.ServerConf) (*kubernetes.Agent, error) {
-	if sc.IngressCluster == "kubeconfig" && sc.SelfKubeconfig != "" {
-		agent, err := local.GetSelfAgentFromFileConfig(sc.SelfKubeconfig)
-
-		if err != nil {
-			return nil, fmt.Errorf("could not get in-cluster agent: %v", err)
-		}
-
-		return agent, nil
-	} else if sc.ProvisionerCluster == "kubeconfig" {
-		return nil, fmt.Errorf(`"kubeconfig" cluster option requires path to kubeconfig`)
-	}
-
-	agent, _ := kubernetes.GetAgentInClusterConfig()
-
-	return agent, nil
-}

+ 1 - 0
api/types/release.go

@@ -17,6 +17,7 @@ type Release struct {
 type PorterRelease struct {
 	ID              uint             `json:"id"`
 	WebhookToken    string           `json:"webhook_token"`
+	LatestVersion   string           `json:"latest_version"`
 	GitActionConfig *GitActionConfig `json:"git_action_config,omitempty"`
 	ImageRepoURI    string           `json:"image_repo_uri"`
 }

+ 2 - 5
build/Dockerfile.osx

@@ -1,17 +1,14 @@
-ARG GO_VERSION=1.13.15
+ARG GO_VERSION=1.16
 
-FROM dockercore/golang-cross:${GO_VERSION}
+FROM golang:${GO_VERSION}
 
 RUN apt-get update && apt-get install -y zip unzip
 
 WORKDIR /go/src/github.com/docker/cli
 COPY    . .
 
-ENV CGO_ENABLED 1
 ENV GOOS darwin
 ENV GOARCH amd64
-ENV CC o64-clang
-ENV CXX o64-clang++
 
 RUN chmod +x ./scripts/build/osx.sh
 

+ 2 - 4
build/Dockerfile.win

@@ -1,14 +1,12 @@
-ARG GO_VERSION=1.13.15
+ARG GO_VERSION=1.16
 
-FROM	dockercore/golang-cross:${GO_VERSION}
+FROM golang:${GO_VERSION}
 
 RUN apt-get update && apt-get install -y zip unzip
 
 WORKDIR /go/src/github.com/docker/cli
 COPY    . .
 
-ENV CC x86_64-w64-mingw32-gcc
-ENV CGO_ENABLED 1
 ENV GOOS windows 
 ENV GOARCH amd64
 

+ 5 - 0
dashboard/package-lock.json

@@ -5073,6 +5073,11 @@
       "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz",
       "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA=="
     },
+    "cohere-js": {
+      "version": "1.0.19",
+      "resolved": "https://registry.npmjs.org/cohere-js/-/cohere-js-1.0.19.tgz",
+      "integrity": "sha512-2XVX2LUKHjbJ4GCsnizXnAVHZfq9RM1RmHl8zE4G2ORdXmDpzSx5i0UIj/0GZ3AwjKIlYsrGA4kdCGT+WapjPQ=="
+    },
     "collection-visit": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",

+ 1 - 0
dashboard/package.json

@@ -22,6 +22,7 @@
     "axios": "^0.20.0",
     "brace": "^0.11.1",
     "clipboard": "^2.0.8",
+    "cohere-js": "^1.0.19",
     "core-js": "^3.16.1",
     "d3-array": "^2.11.0",
     "d3-time-format": "^3.0.0",

+ 9 - 7
dashboard/src/components/repo-selector/RepoList.tsx

@@ -151,13 +151,15 @@ const RepoList: React.FC<Props> = ({
       }
 
       if (accessData.accounts?.length === 0) {
-        <LoadingWrapper>
-          No connected Github repos found. You can
-          <A href={"/api/integrations/github-app/install"}>
-            Install Porter in more repositories
-          </A>
-          .
-        </LoadingWrapper>;
+        return (
+          <LoadingWrapper>
+            No connected Github repos found. You can
+            <A href={"/api/integrations/github-app/install"}>
+              Install Porter in more repositories
+            </A>
+            .
+          </LoadingWrapper>
+        );
       }
     }
 

+ 36 - 0
dashboard/src/index.html

@@ -3,6 +3,42 @@
   <head>
     <title>Porter | Dashboard</title>
 
+    <script>
+      !(function () {
+        var e = (window.Cohere = window.Cohere || []);
+        if (e.invoked) console.error("Tried to load Cohere twice");
+        else {
+          (e.invoked = !0),
+            (e.snippet = "0.2"),
+            (e.methods = [
+              "init",
+              "identify",
+              "stop",
+              "showCode",
+              "getSessionUrl",
+              "makeCall",
+              "addCallStatusListener",
+              "removeCallStatusListener",
+              "widget",
+            ]),
+            e.methods.forEach(function (o) {
+              e[o] = function () {
+                var t = Array.prototype.slice.call(arguments);
+                t.unshift(o), e.push(t);
+              };
+            });
+          var o = document.createElement("script");
+          (o.type = "text/javascript"),
+            (o.async = !0),
+            (o.src = "https://static.cohere.so/main.js"),
+            (o.crossOrigin = "anonymous");
+          var t = document.getElementsByTagName("script")[0];
+          t.parentNode.insertBefore(o, t);
+        }
+      })();
+      window.Cohere.init("_A-2HNgriISqaQq4yzTxM8V-");
+    </script>
+
     <script>
       !(function () {
         var analytics = (window.analytics = window.analytics || []);

+ 10 - 0
dashboard/src/main/Main.tsx

@@ -3,6 +3,9 @@ import { Route, Redirect, Switch } from "react-router-dom";
 
 import api from "shared/api";
 import { Context } from "shared/Context";
+import Cohere from "cohere-js";
+
+Cohere.init(process.env.COHERE_API_KEY);
 
 import ResetPasswordInit from "./auth/ResetPasswordInit";
 import ResetPasswordFinalize from "./auth/ResetPasswordFinalize";
@@ -42,6 +45,13 @@ export default class Main extends Component<PropsType, StateType> {
       .checkAuth("", {}, {})
       .then((res) => {
         if (res && res?.data) {
+          Cohere.identify(
+            res?.data?.id, 
+            {
+              displayName: res?.data?.email,
+              email: res?.data?.email, 
+            }
+          );
           setUser(res?.data?.id, res?.data?.email);
           this.setState({
             isLoggedIn: true,

+ 1 - 5
dashboard/src/main/home/NoClusterPlaceholder.tsx

@@ -25,11 +25,7 @@ class NoClusterPlaceholder extends Component<PropsType, StateType> {
         <br />
         <br />
         1. If you're deploying from a repo{" "}
-        <A
-          onClick={() =>
-            window.open(`/api/oauth/projects/${currentProject.id}/github`)
-          }
-        >
+        <A href={"/api/integrations/github-app/oauth"}>
           link your GitHub account
         </A>
         <br />

+ 1 - 2
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -608,14 +608,13 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
       await api.upgradeChartValues(
         "<token>",
         {
-          namespace: currentChart.namespace,
-          storage: StorageType.Secret,
           values: valuesYaml,
           version: version,
         },
         {
           id: this.context.currentProject.id,
           name: currentChart.name,
+          namespace: currentChart.namespace,
           cluster_id: this.context.currentCluster.id,
         }
       );

+ 1 - 1
dashboard/src/main/home/launch/launch-flow/SourcePage.tsx

@@ -275,7 +275,7 @@ class SourcePage extends Component<PropsType, StateType> {
         <Helper>
           Learn more about
           <Highlight
-            href="https://docs.porter.run/docs/addons"
+            href="https://docs.porter.run/docs/applications"
             target="_blank"
           >
             deploying services to Porter

+ 1 - 1
dashboard/src/main/home/modals/AccountSettingsModal.tsx

@@ -69,7 +69,7 @@ const AccountSettingsModal = () => {
           )}
 
           {/* Will be styled (and show what account is connected) later */}
-          {!accessError && accessData.accounts?.length >= 0 && (
+          {!accessError && accessData.username && (
             <Placeholder>
               <User>
                 You are currently authorized as <B>{accessData.username}</B> and

+ 1 - 0
dashboard/src/shared/error_handling/window_error_handling.ts

@@ -1,4 +1,5 @@
 import { stackFramesToString } from "./stack_trace_utils";
+import StackTrace from "stacktrace-js";
 import * as Sentry from "@sentry/react";
 
 export function EnableErrorHandling() {

+ 153 - 0
internal/integrations/powerdns/powerdns.go

@@ -0,0 +1,153 @@
+package powerdns
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+)
+
+// Client contains an API client for a PowerDNS server
+type Client struct {
+	apiKey    string
+	serverURL string
+	runDomain string
+
+	httpClient *http.Client
+}
+
+// NewClient creates a new bind API client
+func NewClient(serverURL, apiKey, runDomain string) *Client {
+	httpClient := &http.Client{
+		Timeout: time.Minute,
+	}
+
+	return &Client{apiKey, serverURL, runDomain, httpClient}
+}
+
+// RecordData represents the data required to create or delete an A/CNAME record
+// for the nameserver
+type RecordData struct {
+	RRSets []RR `json:"rrsets"`
+}
+
+type RR struct {
+	Name       string   `json:"name"`
+	Type       string   `json:"type"`
+	ChangeType string   `json:"changetype"`
+	TTL        uint     `json:"ttl"`
+	Records    []Record `json:"records"`
+}
+
+type Record struct {
+	Content  string `json:"content"`
+	Disabled bool   `json:"disabled"`
+	Name     string `json:"name"`
+	Type     string `json:"type"`
+	Priority uint   `json:"priority"`
+}
+
+// CreateCNAMERecord creates a new CNAME record for the nameserver
+func (c *Client) CreateCNAMERecord(value, hostname string) error {
+	valueC := canonicalize(value)
+	hostnameC := canonicalize(hostname)
+
+	return c.sendRequest("PATCH", &RecordData{
+		RRSets: []RR{{
+			Name:       hostnameC,
+			Type:       "CNAME",
+			ChangeType: "REPLACE",
+			TTL:        300,
+			Records: []Record{{
+				Content:  valueC,
+				Disabled: false,
+				Name:     hostnameC,
+				Type:     "CNAME",
+				Priority: 0,
+			}},
+		}},
+	})
+}
+
+// CreateARecord creates a new A record for the nameserver
+func (c *Client) CreateARecord(value, hostname string) error {
+	hostnameC := canonicalize(hostname)
+
+	return c.sendRequest("PATCH", &RecordData{
+		RRSets: []RR{{
+			Name:       hostnameC,
+			Type:       "A",
+			ChangeType: "REPLACE",
+			TTL:        300,
+			Records: []Record{{
+				Content:  value,
+				Disabled: false,
+				Name:     hostnameC,
+				Type:     "A",
+				Priority: 0,
+			}},
+		}},
+	})
+}
+
+func canonicalize(value string) string {
+	// if the string ends in a period, return
+	if value[len(value)-1:] == "." {
+		return value
+	}
+
+	return fmt.Sprintf("%s.", value)
+}
+
+func (c *Client) sendRequest(method string, data *RecordData) error {
+	reqURL, err := url.Parse(c.serverURL)
+
+	if err != nil {
+		return nil
+	}
+
+	reqURL.Path = fmt.Sprintf("/api/v1/servers/localhost/zones/%s", c.runDomain)
+
+	strData, err := json.Marshal(data)
+
+	if err != nil {
+		return err
+	}
+
+	req, err := http.NewRequest(
+		method,
+		reqURL.String(),
+		strings.NewReader(string(strData)),
+	)
+
+	if err != nil {
+		return err
+	}
+
+	req.Header.Set("Content-Type", "application/json; charset=utf-8")
+	req.Header.Set("Accept", "application/json; charset=utf-8")
+	req.Header.Set("X-Api-Key", c.apiKey)
+
+	res, err := c.httpClient.Do(req)
+
+	if err != nil {
+		return err
+	}
+
+	defer res.Body.Close()
+
+	if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
+		resBytes, err := ioutil.ReadAll(res.Body)
+
+		if err != nil {
+			return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error())
+		}
+
+		return fmt.Errorf("request failed with status code %d: %s\n", res.StatusCode, string(resBytes))
+	}
+
+	return nil
+}

+ 1 - 4
internal/kubernetes/agent.go

@@ -536,15 +536,12 @@ func (a *Agent) GetPodLogs(namespace string, name string, rw *websocket.Websocke
 
 	// see if container is ready and able to open a stream. If not, wait for container
 	// to be ready.
-	err, isExited := a.waitForPod(pod)
+	err, _ = a.waitForPod(pod)
 
 	if err != nil && goerrors.Is(err, IsNotFoundError) {
 		return IsNotFoundError
 	} else if err != nil {
 		return fmt.Errorf("Cannot get logs from pod %s: %s", name, err.Error())
-	} else if isExited {
-		// if exited, we return nil and simply close the stream
-		return nil
 	}
 
 	container := pod.Spec.Containers[0].Name

+ 6 - 140
internal/kubernetes/domain/domain.go

@@ -6,14 +6,13 @@ import (
 	"net"
 	"strings"
 
+	"github.com/porter-dev/porter/internal/integrations/powerdns"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 	v1 "k8s.io/api/core/v1"
-	"k8s.io/api/extensions/v1beta1"
 	"k8s.io/client-go/kubernetes"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/util/intstr"
 )
 
 // GetNGINXIngressServiceIP retrieves the external address of the nginx-ingress service
@@ -80,147 +79,14 @@ func (c *CreateDNSRecordConfig) NewDNSRecordForEndpoint() *models.DNSRecord {
 	}
 }
 
-func (e *DNSRecord) CreateDomain(clientset kubernetes.Interface) error {
-	// determine if IP address or domain
-	err := e.createIngress(clientset)
-
-	if err != nil {
-		return err
-	}
-
-	return e.createServiceWithEndpoint(clientset)
-}
-
-func (e *DNSRecord) createIngress(clientset kubernetes.Interface) error {
-	_, err := clientset.ExtensionsV1beta1().Ingresses("default").Create(
-		context.TODO(),
-		&v1beta1.Ingress{
-			ObjectMeta: metav1.ObjectMeta{
-				Annotations: map[string]string{
-					"kubernetes.io/ingress.class":                  "nginx",
-					"nginx.ingress.kubernetes.io/ssl-redirect":     "true",
-					"nginx.ingress.kubernetes.io/backend-protocol": "HTTPS",
-					"nginx.ingress.kubernetes.io/upstream-vhost":   e.Hostname,
-				},
-				Name:      e.SubdomainPrefix,
-				Namespace: "default",
-			},
-			Spec: v1beta1.IngressSpec{
-				TLS: []v1beta1.IngressTLS{
-					{
-						Hosts:      []string{fmt.Sprintf("%s.%s", e.SubdomainPrefix, e.RootDomain)},
-						SecretName: "wildcard-cert-tls",
-					},
-				},
-				Rules: []v1beta1.IngressRule{
-					{
-						Host: fmt.Sprintf("%s.%s", e.SubdomainPrefix, e.RootDomain),
-						IngressRuleValue: v1beta1.IngressRuleValue{
-							HTTP: &v1beta1.HTTPIngressRuleValue{
-								Paths: []v1beta1.HTTPIngressPath{
-									{
-										Backend: v1beta1.IngressBackend{
-											ServiceName: e.SubdomainPrefix,
-											ServicePort: intstr.IntOrString{
-												Type:   intstr.Int,
-												IntVal: 443,
-											},
-										},
-									},
-								},
-							},
-						},
-					},
-				},
-			},
-		},
-		metav1.CreateOptions{},
-	)
-
-	return err
-}
-
-func (e *DNSRecord) createServiceWithEndpoint(clientset kubernetes.Interface) error {
-	// determine if endpoint needs to be created or external name is ok
+// CreateDomain creates a new record for the vanity domain
+func (e *DNSRecord) CreateDomain(powerDNSClient *powerdns.Client) error {
 	isIPv4 := net.ParseIP(e.Endpoint) != nil
-
-	svcSpec := v1.ServiceSpec{
-		Ports: []v1.ServicePort{
-			{
-				Port: 80,
-				TargetPort: intstr.IntOrString{
-					Type:   intstr.Int,
-					IntVal: 80,
-				},
-				Name: "http",
-			},
-			{
-				Port: 443,
-				TargetPort: intstr.IntOrString{
-					Type:   intstr.Int,
-					IntVal: 443,
-				},
-				Name: "https",
-			},
-		},
-	}
-
-	// case service spec on ipv4
-	if isIPv4 {
-		svcSpec.ClusterIP = "None"
-	} else {
-		svcSpec.Type = "ExternalName"
-		svcSpec.ExternalName = e.Endpoint
-	}
-
-	// create service
-	_, err := clientset.CoreV1().Services("default").Create(
-		context.TODO(),
-		&v1.Service{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      e.SubdomainPrefix,
-				Namespace: "default",
-			},
-			Spec: svcSpec,
-		},
-		metav1.CreateOptions{},
-	)
-
-	if err != nil {
-		return err
-	}
+	domain := fmt.Sprintf("%s.%s", e.SubdomainPrefix, e.RootDomain)
 
 	if isIPv4 {
-		_, err = clientset.CoreV1().Endpoints("default").Create(
-			context.TODO(),
-			&v1.Endpoints{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      e.SubdomainPrefix,
-					Namespace: "default",
-				},
-				Subsets: []v1.EndpointSubset{
-					{
-						Addresses: []v1.EndpointAddress{
-							{
-								IP: e.Endpoint,
-							},
-						},
-						Ports: []v1.EndpointPort{
-							{
-								Name: "http",
-								Port: 80,
-							},
-							{
-								Name: "https",
-								Port: 443,
-							},
-						},
-					},
-				},
-			},
-			metav1.CreateOptions{},
-		)
+		return powerDNSClient.CreateARecord(e.Endpoint, domain)
 	}
 
-	return err
+	return powerDNSClient.CreateCNAMERecord(e.Endpoint, domain)
 }

+ 7 - 2
scripts/build/win.sh

@@ -2,7 +2,12 @@
 #
 # Accepts the version as an argument
 
-go build -ldflags="-w -s -X 'github.com/porter-dev/porter/cli/cmd.Version=$1'" -a -tags cli -o ./porter.exe ./cli
+go build -ldflags="-w -s -X 'github.com/porter-dev/porter/cli/cmd.Version=$1'" -a -tags cli -o ./porter.exe ./cli &
+go build -ldflags="-w -s -X 'main.Version=$1'" -a -o ./docker-credential-porter.exe ./cmd/docker-credential-porter/ &
+go build -ldflags="-w -s -X 'main.Version=$1'" -a -o ./portersvr.exe ./cmd/app/ &
+wait
 
 mkdir -p /release/windows
-zip --junk-paths /release/windows/porter_$1_Windows_x86_64.zip ./porter.exe
+zip --junk-paths /release/windows/porter_$1_Windows_x86_64.zip ./porter.exe
+zip --junk-paths /release/windows/portersvr_$1_Windows_x86_64.zip ./portersvr.exe
+zip --junk-paths /release/windows/docker-credential-porter_$1_Windows_x86_64.zip ./docker-credential-porter.exe