Просмотр исходного кода

Merge pull request #879 from porter-dev/master

Env group + var changes -> staging
abelanger5 4 лет назад
Родитель
Сommit
51158a6da5

+ 7 - 2
.github/workflows/production.yaml

@@ -13,6 +13,12 @@ jobs:
           project_id: ${{ secrets.GCP_PROJECT_ID }}
           service_account_key: ${{ secrets.GCP_SA_KEY }}
           export_default_credentials: true
+      - name: Configure AWS Credentials
+        uses: aws-actions/configure-aws-credentials@v1
+        with:
+          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+          aws-region: ${{ secrets.AWS_REGION }}
       - name: Install kubectl
         uses: azure/setup-kubectl@v1
       - name: Log in to gcloud CLI
@@ -42,7 +48,6 @@ jobs:
           docker push gcr.io/porter-dev-273614/porter:latest
       - name: Deploy to cluster
         run: |
-          gcloud container clusters get-credentials \
-            production-2 --region us-central1 --project ${{ secrets.GCP_PROJECT_ID }}
+          aws eks --region ${{ secrets.AWS_REGION }} update-kubeconfig --name production-2
             
           kubectl rollout restart deployment/porter

+ 4 - 0
cli/cmd/docker.go

@@ -138,6 +138,10 @@ func dockerConfig(user *api.AuthCheckResponse, client *api.Client, args []string
 		configFile.CredentialHelpers = make(map[string]string)
 	}
 
+	if configFile.AuthConfigs == nil {
+		configFile.AuthConfigs = make(map[string]types.AuthConfig)
+	}
+
 	for _, regURL := range regToAdd {
 		// if this is a dockerhub registry, see if an auth config has already been generated
 		// for index.docker.io

+ 22 - 8
dashboard/src/components/SaveButton.tsx

@@ -22,25 +22,29 @@ export default class SaveButton extends Component<PropsType, StateType> {
       if (this.props.status === "successful") {
         return (
           <StatusWrapper successful={true}>
-            <i className="material-icons">done</i> Successfully updated
+            <i className="material-icons">done</i>
+            <StatusTextWrapper>Successfully updated</StatusTextWrapper>
           </StatusWrapper>
         );
       } else if (this.props.status === "loading") {
         return (
           <StatusWrapper successful={false}>
-            <LoadingGif src={loading} /> Updating . . .
+            <LoadingGif src={loading} />
+            <StatusTextWrapper>Updating . . .</StatusTextWrapper>
           </StatusWrapper>
         );
       } else if (this.props.status === "error") {
         return (
           <StatusWrapper successful={false}>
-            <i className="material-icons">error_outline</i> Could not update
+            <i className="material-icons">error_outline</i>
+            <StatusTextWrapper>Could not update</StatusTextWrapper>
           </StatusWrapper>
         );
       } else {
         return (
           <StatusWrapper successful={false}>
-            <i className="material-icons">error_outline</i> {this.props.status}
+            <i className="material-icons">error_outline</i>
+            <StatusTextWrapper>{this.props.status}</StatusTextWrapper>
           </StatusWrapper>
         );
       }
@@ -54,7 +58,7 @@ export default class SaveButton extends Component<PropsType, StateType> {
   render() {
     return (
       <ButtonWrapper makeFlush={this.props.makeFlush}>
-        {this.renderStatus()}
+        <div>{this.renderStatus()}</div>
         <Button
           disabled={this.props.disabled}
           onClick={this.props.onClick}
@@ -74,6 +78,15 @@ const LoadingGif = styled.img`
   margin-bottom: 0px;
 `;
 
+const StatusTextWrapper = styled.p`
+  display: -webkit-box;
+  line-clamp: 2;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  line-height: 19px;
+  margin: 0;
+`;
+
 const StatusWrapper = styled.div`
   display: flex;
   align-items: center;
@@ -81,16 +94,14 @@ const StatusWrapper = styled.div`
   font-size: 13px;
   color: #ffffff55;
   margin-right: 25px;
-  padding: 0 10px;
-
   max-width: 500px;
-  white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
 
   > i {
     font-size: 18px;
     margin-right: 10px;
+    float: left;
     color: ${(props: { successful: boolean }) =>
       props.successful ? "#4797ff" : "#fcba03"};
   }
@@ -114,11 +125,13 @@ const ButtonWrapper = styled.div`
   display: flex;
   align-items: center;
   position: absolute;
+  justify-content: flex-end;
   ${(props: { makeFlush: boolean }) => {
     if (!props.makeFlush) {
       return `
         bottom: 25px;
         right: 27px;
+        left: 27px;
       `;
     }
     return `
@@ -134,6 +147,7 @@ const Button = styled.button`
   font-weight: 500;
   font-family: "Work Sans", sans-serif;
   color: white;
+  flex: 0 0 auto;
   padding: 6px 20px 7px 20px;
   text-align: left;
   border: 0;

+ 14 - 14
dashboard/src/components/values-form/KeyValueArray.tsx

@@ -8,6 +8,11 @@ import sliders from "assets/sliders.svg";
 import upload from "assets/upload.svg";
 import { keysIn } from "lodash";
 
+export type KeyValue = {
+  key: string;
+  value: string;
+};
+
 type PropsType = {
   label?: string;
   values: any;
@@ -51,15 +56,8 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
     return obj;
   };
 
-  objectToValues = (obj: any) => {
-    let values = [] as any[];
-    Object.keys(obj).forEach((key: string, i: number) => {
-      let entry = {} as any;
-      entry.key = key;
-      entry.value = obj[key];
-      values.push(entry);
-    });
-    return values;
+  objectToValues = (obj: Record<string, string>): KeyValue[] => {
+    return Object.entries(obj).map(([key, value]) => ({ key, value }));
   };
 
   renderDeleteButton = (i: number) => {
@@ -148,16 +146,18 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
       return (
         <Modal
           onRequestClose={() => this.setState({ showEnvModal: false })}
-          width="665px"
-          height="342px"
+          width="765px"
+          height="542px"
         >
           <LoadEnvGroupModal
+            existingValues={this.props.values}
             namespace={this.props.externalValues?.namespace}
             clusterId={this.props.externalValues?.clusterId}
             closeModal={() => this.setState({ showEnvModal: false })}
-            setValues={(values: any) => {
-              this.props.setValues(values);
-              this.setState({ values: this.objectToValues(values) });
+            setValues={(values) => {
+              const newValues = { ...this.props.values, ...values };
+              this.props.setValues(newValues);
+              this.setState({ values: this.objectToValues(newValues) });
             }}
           />
         </Modal>

+ 13 - 1
dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroup.tsx

@@ -5,8 +5,13 @@ import key from "assets/key.svg";
 
 import { Context } from "shared/Context";
 
+export type EnvGroupData = {
+  data: Record<string, string>;
+  metadata: any;
+};
+
 type PropsType = {
-  envGroup: any;
+  envGroup: EnvGroupData;
   setExpanded: () => void;
 };
 
@@ -71,6 +76,13 @@ export default class EnvGroup extends Component<PropsType, StateType> {
   }
 }
 
+export function formattedEnvironmentValue(value: string) {
+  if (value.startsWith("PORTERSECRET_")) {
+    return "••••";
+  }
+  return value;
+}
+
 EnvGroup.contextType = Context;
 
 const BottomWrapper = styled.div`

+ 2 - 1
dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupArray.tsx

@@ -204,7 +204,8 @@ export default class EnvGroupArray extends Component<PropsType, StateType> {
     for (let key in envObj) {
       for (var i = 0; i < this.props.values.length; i++) {
         let existingKey = this.props.values[i]["key"];
-        if (key === existingKey) {
+        let isExistingKeyDeleted = this.props.values[i]["deleted"];
+        if (key === existingKey && !isExistingKeyDeleted) {
           _values[i]["value"] = envObj[key];
           push = false;
         }

+ 177 - 12
dashboard/src/main/home/modals/LoadEnvGroupModal.tsx

@@ -1,5 +1,5 @@
 import React, { Component } from "react";
-import styled from "styled-components";
+import styled, { css } from "styled-components";
 import close from "assets/close.png";
 import sliders from "assets/sliders.svg";
 
@@ -8,19 +8,25 @@ import { Context } from "shared/Context";
 
 import Loading from "components/Loading";
 import SaveButton from "components/SaveButton";
+import { KeyValue } from "components/values-form/KeyValueArray";
+import {
+  EnvGroupData,
+  formattedEnvironmentValue,
+} from "../cluster-dashboard/env-groups/EnvGroup";
 
 type PropsType = {
   namespace: string;
   clusterId: number;
   closeModal: () => void;
-  setValues: (values: any) => void;
+  existingValues: Record<string, string>;
+  setValues: (values: Record<string, string>) => void;
 };
 
 type StateType = {
   envGroups: any[];
   loading: boolean;
   error: boolean;
-  selectedEnvGroup: any;
+  selectedEnvGroup: EnvGroupData | null;
   buttonStatus: string;
 };
 
@@ -29,7 +35,7 @@ export default class LoadEnvGroupModal extends Component<PropsType, StateType> {
     envGroups: [] as any[],
     loading: true,
     error: false,
-    selectedEnvGroup: null as any,
+    selectedEnvGroup: null as EnvGroupData | null,
     buttonStatus: "",
   };
 
@@ -96,7 +102,56 @@ export default class LoadEnvGroupModal extends Component<PropsType, StateType> {
     }
   };
 
+  potentiallyOverriddenKeys(incoming: Record<string, string>): KeyValue[] {
+    return Object.entries(incoming)
+      .filter(([key]) => this.props.existingValues[key])
+      .map(([key, value]) => ({ key, value }));
+  }
+
+  saveButtonStatus(hasClashingKeys: boolean): string {
+    if (!this.state.selectedEnvGroup) {
+      return "No env group selected";
+    }
+    if (hasClashingKeys) {
+      return "There are variables defined in this group that will override existing variables.";
+    }
+  }
+
+  renderEnvGroupPreview(clashingKeys: KeyValue[]) {
+    const emptyValue = <i>Empty value</i>;
+    return (
+      <PossibleClashingKeys>
+        {clashingKeys.map(({ key, value }, i) => (
+          <ClashingKeyItem key={key}>
+            <ClashingKeyTop>
+              <ClashIconWrapper>
+                <ClashIcon className="material-icons">sync_problem</ClashIcon>
+              </ClashIconWrapper>
+              <ClashingKeyExplanation>
+                <b>{key}</b> is defined in both environments
+              </ClashingKeyExplanation>
+            </ClashingKeyTop>
+            <ClashingKeyDefinitions>
+              <ClashingKeyLabel>Old</ClashingKeyLabel>
+              <ClashingKeyValue>
+                {formattedEnvironmentValue(this.props.existingValues[key]) ||
+                  emptyValue}
+              </ClashingKeyValue>
+              <ClashingKeyLabel>New</ClashingKeyLabel>
+              <ClashingKeyValue>
+                {formattedEnvironmentValue(value) || emptyValue}
+              </ClashingKeyValue>
+            </ClashingKeyDefinitions>
+          </ClashingKeyItem>
+        ))}
+      </PossibleClashingKeys>
+    );
+  }
+
   render() {
+    const clashingKeys = this.state.selectedEnvGroup
+      ? this.potentiallyOverriddenKeys(this.state.selectedEnvGroup.data)
+      : [];
     return (
       <StyledLoadEnvGroupModal>
         <CloseButton onClick={this.props.closeModal}>
@@ -109,16 +164,35 @@ export default class LoadEnvGroupModal extends Component<PropsType, StateType> {
           {this.props.namespace}).
         </Subtitle>
 
-        <EnvGroupList>{this.renderEnvGroupList()}</EnvGroupList>
+        <GroupModalSections>
+          <SidebarSection $expanded={!this.state.selectedEnvGroup}>
+            <EnvGroupList>{this.renderEnvGroupList()}</EnvGroupList>
+          </SidebarSection>
+
+          {this.state.selectedEnvGroup && (
+            <SidebarSection>
+              <GroupEnvPreview>
+                {Object.entries(this.state.selectedEnvGroup.data)
+                  .map(
+                    ([key, value]) =>
+                      `${key}=${formattedEnvironmentValue(value)}`
+                  )
+                  .join("\n")}
+              </GroupEnvPreview>
+              {clashingKeys.length > 0 && (
+                <>
+                  <ClashingKeyRowDivider />
+                  {this.renderEnvGroupPreview(clashingKeys)}
+                </>
+              )}
+            </SidebarSection>
+          )}
+        </GroupModalSections>
 
         <SaveButton
           disabled={!this.state.selectedEnvGroup}
           text="Load Selected Env Group"
-          status={
-            !this.state.selectedEnvGroup
-              ? "No env group selected"
-              : "Existing env variables will be overidden"
-          }
+          status={this.saveButtonStatus(clashingKeys.length > 0)}
           onClick={this.onSubmit}
         />
       </StyledLoadEnvGroupModal>
@@ -128,6 +202,39 @@ export default class LoadEnvGroupModal extends Component<PropsType, StateType> {
 
 LoadEnvGroupModal.contextType = Context;
 
+const SidebarSection = styled.section<{ $expanded?: boolean }>`
+  height: 100%;
+  overflow-y: auto;
+  ${(props) =>
+    props.$expanded &&
+    css`
+      grid-column: span 2;
+    `}
+`;
+
+const GroupEnvPreview = styled.pre`
+  font-family: monospace;
+  margin: 0 0 10px 0;
+  white-space: pre-line;
+  word-break: break-word;
+  user-select: text;
+`;
+
+const ClashingKeyExplanation = styled.div`
+  padding: 10px 15px;
+`;
+
+const ClashIconWrapper = styled.div`
+  padding: 10px;
+  background: #3d4048;
+  display: flex;
+  align-items: center;
+`;
+
+const ClashIcon = styled.i`
+  font-size: 18px;
+`;
+
 const Placeholder = styled.div`
   width: 100%;
   height: 150px;
@@ -168,13 +275,65 @@ const EnvGroupRow = styled.div<{ lastItem?: boolean; isSelected: boolean }>`
   }
 `;
 
-const EnvGroupList = styled.div`
+const GroupModalSections = styled.div`
   margin-top: 20px;
+  width: 100%;
+  height: 100%;
+  display: grid;
+  gap: 10px;
+  grid-template-columns: 1fr 1fr;
+  max-height: 365px;
+`;
+
+const PossibleClashingKeys = styled.ul`
+  appearance: none;
+  color: #aaaabb;
+  margin: 0;
+  padding-inline-start: 0;
+  list-style: none;
+  > *:not(:last-child) {
+    margin-bottom: 8px;
+  }
+`;
+
+const ClashingKeyItem = styled.li`
+  overflow: hidden;
+  border: 1px solid #292c31;
+  border-radius: 5px;
+`;
+
+const ClashingKeyRowDivider = styled.hr`
+  margin: 16px 0;
+  border: 1px solid #27292f;
+`;
+
+const ClashingKeyDefinitions = styled.div`
+  grid-template-columns: min-content auto;
+  padding: 5px 0;
+  column-gap: 6px;
+  display: grid;
+`;
+
+const ClashingKeyLabel = styled.p`
+  margin: 0px;
+  font-weight: bold;
+  padding: 5px 10px;
+  white-space: nowrap;
+`;
+
+const ClashingKeyValue = styled.p`
+  margin: 0px;
+  display: flex;
+  padding: 0;
+  align-items: center;
+  word-break: break-word;
+`;
+
+const EnvGroupList = styled.div`
   width: 100%;
   border-radius: 3px;
   background: #ffffff11;
   border: 1px solid #ffffff44;
-  max-height: 160px;
   overflow-y: auto;
 `;
 
@@ -185,6 +344,12 @@ const Subtitle = styled.div`
   color: #aaaabb;
 `;
 
+const ClashingKeyTop = styled.div`
+  background: #2e3035;
+  display: flex;
+  align-items: stretch;
+`;
+
 const ModalTitle = styled.div`
   margin: 0px 0px 13px;
   display: flex;

+ 3 - 4
docs/guides/authorization-and-team-management.md

@@ -10,10 +10,9 @@ To add a new collaborator to a Porter project, you must be logged in with an **A
 
 ![image](https://user-images.githubusercontent.com/23369263/125147098-b00f3100-e0ff-11eb-8579-cc28c1a0badc.png)
 
-**DISCLAIMER**
-
-The user has to register or access porter with the same email that the invitation was sent to.
-If not, it will not be able to look your project.
+> 🚧
+> 
+> If the user does not have a Porter account, they will be asked to register. After registering, if they are not automatically added to the project, the user should **click the invite link again**.  
 
 # Changing Collaborator Permissions