Forráskód Böngészése

add github permission casing to preview environments and style

Alexander Belanger 4 éve
szülő
commit
19beb96151

+ 43 - 0
api/server/handlers/gitinstallation/get_permissions.go

@@ -0,0 +1,43 @@
+package gitinstallation
+
+import (
+	"net/http"
+
+	"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"
+)
+
+type GithubGetPermissionsHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewGithubGetPermissionsHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GithubGetPermissionsHandler {
+	return &GithubGetPermissionsHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *GithubGetPermissionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	p, err := GetGithubAppPermissions(c.Config(), r)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, &types.GitInstallationPermission{
+		PreviewEnvironments: p.Administration == "write" &&
+			p.Deployments == "write" &&
+			p.Environments == "write" &&
+			p.PullRequests == "write",
+	})
+}

+ 60 - 0
api/server/handlers/gitinstallation/helpers.go

@@ -1,6 +1,7 @@
 package gitinstallation
 
 import (
+	"context"
 	"net/http"
 	"net/url"
 
@@ -75,6 +76,65 @@ func GetGithubAppClientFromRequest(config *config.Config, r *http.Request) (*git
 	return github.NewClient(&http.Client{Transport: itr}), nil
 }
 
+type GithubAppPermissions struct {
+	Actions        string
+	Administration string
+	Contents       string
+	Deployments    string
+	Environments   string
+	Metadata       string
+	PullRequests   string
+	Secrets        string
+	Workflows      string
+}
+
+// GetGithubAppClientFromRequest gets the github app installation id from the request and authenticates
+// using it and a private key file
+func GetGithubAppPermissions(config *config.Config, r *http.Request) (*GithubAppPermissions, error) {
+	// get installation id from context
+	ga, _ := r.Context().Value(types.GitInstallationScope).(*integrations.GithubAppInstallation)
+
+	itr, err := ghinstallation.NewKeyFromFile(
+		http.DefaultTransport,
+		config.GithubAppConf.AppID,
+		ga.InstallationID,
+		config.GithubAppConf.SecretPath,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// need to request the token before permissions can be verified
+	_, err = itr.Token(context.Background())
+
+	if err != nil {
+		return nil, err
+	}
+
+	permissions, err := itr.Permissions()
+
+	return &GithubAppPermissions{
+		Actions:        permissionToString(permissions.Actions),
+		Administration: permissionToString(permissions.Administration),
+		Contents:       permissionToString(permissions.Contents),
+		Deployments:    permissionToString(permissions.Deployments),
+		Environments:   permissionToString(permissions.Environments),
+		Metadata:       permissionToString(permissions.Metadata),
+		PullRequests:   permissionToString(permissions.PullRequests),
+		Secrets:        permissionToString(permissions.Secrets),
+		Workflows:      permissionToString(permissions.Workflows),
+	}, err
+}
+
+func permissionToString(permission *string) string {
+	if permission == nil {
+		return ""
+	}
+
+	return *permission
+}
+
 // GetOwnerAndNameParams gets the owner and name ref for the Github repo
 func GetOwnerAndNameParams(c handlers.PorterHandler, w http.ResponseWriter, r *http.Request) (string, string, bool) {
 	owner, reqErr := requestutils.GetURLParamString(r, types.URLParamGitRepoOwner)

+ 29 - 0
api/server/router/git_installation.go

@@ -83,6 +83,35 @@ func getGitInstallationRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/gitrepos/{git_installation_id}/permissions -> gitinstallation.NewGithubGetPermissionsHandler
+	getPermissionsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/permissions",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.GitInstallationScope,
+			},
+		},
+	)
+
+	getPermissionsHandler := gitinstallation.NewGithubGetPermissionsHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: getPermissionsEndpoint,
+		Handler:  getPermissionsHandler,
+		Router:   r,
+	})
+
 	// POST /api/projects/{project_id}/gitrepos/{git_installation_id}/{owner}/{name}/clusters/{cluster_id} ->
 	// environment.NewCreateEnvironmentHandler
 	createEnvironmentEndpoint := factory.NewAPIEndpoint(

+ 4 - 0
api/types/git_installation.go

@@ -65,3 +65,7 @@ type GetGithubAppAccountsResponse struct {
 	Username string   `json:"username,omitempty"`
 	Accounts []string `json:"accounts,omitempty"`
 }
+
+type GitInstallationPermission struct {
+	PreviewEnvironments bool `json:"preview_environments"`
+}

+ 86 - 21
dashboard/src/main/home/cluster-dashboard/dashboard/preview-environments/EnvironmentList.tsx

@@ -11,7 +11,7 @@ import ButtonEnablePREnvironments from "./components/ButtonEnablePREnvironments"
 import ConnectNewRepo from "./components/ConnectNewRepo";
 import Loading from "components/Loading";
 
-import _ from "lodash";
+import _, { flatMapDepth } from "lodash";
 import EnvironmentCard from "./components/EnvironmentCard";
 
 export type PRDeployment = {
@@ -42,6 +42,8 @@ export type Environment = {
 const EnvironmentList = () => {
   const [isLoading, setIsLoading] = useState(true);
   const [hasError, setHasError] = useState(false);
+  const [hasPermissions, setHasPermissions] = useState(false);
+  const [hasPermissionsLoaded, setHasPermissionsLoaded] = useState(false);
   const [environmentList, setEnvironmentList] = useState<Environment[]>([]);
   const [deploymentList, setDeploymentList] = useState<PRDeployment[]>([]);
   const [statusSelectorVal, setStatusSelectorVal] = useState<string>("active");
@@ -110,6 +112,48 @@ const EnvironmentList = () => {
     };
   }, [currentProject, currentCluster]);
 
+  useEffect(() => {
+    setHasPermissionsLoaded(false);
+
+    api
+      .getGitRepos("<token>", {}, { project_id: currentProject.id })
+      .then(({ data }) => {
+        Promise.all(
+          data.map((id: number) => {
+            return new Promise((resolve, reject) => {
+              api
+                .getGitRepoPermission(
+                  "<token>",
+                  {},
+                  { project_id: currentProject.id, git_repo_id: id }
+                )
+                .then((res) => {
+                  resolve(res.data);
+                })
+                .catch((err) => {
+                  reject(err);
+                });
+            });
+          })
+        )
+          .then((permissions: any[]) => {
+            let hasPermission =
+              permissions.filter((val) => {
+                return val.preview_environments;
+              }).length >= 1;
+
+            setHasPermissions(hasPermission);
+            setHasPermissionsLoaded(true);
+          })
+          .catch((err) => {
+            console.log(err);
+          });
+      })
+      .catch((err) => {
+        console.log(err);
+      });
+  }, [currentProject, currentCluster]);
+
   useEffect(() => {
     let isSubscribed = true;
     getPRDeploymentList()
@@ -170,7 +214,16 @@ const EnvironmentList = () => {
     );
   }
 
-  if (isLoading) {
+  if (hasPermissionsLoaded && !hasPermissions) {
+    return (
+      <Placeholder>
+        Github App permissions are not up to date. Please review any pending
+        requests to update Github App permissions.
+      </Placeholder>
+    );
+  }
+
+  if (isLoading || !hasPermissionsLoaded) {
     return (
       <Placeholder>
         <Loading />
@@ -184,9 +237,14 @@ const EnvironmentList = () => {
 
   if (!environmentList.length) {
     return (
-      <>
+      <Placeholder>
+        <Header>Preview environments are not enabled on this cluster</Header>
+        <Subheader>
+          In order to use preview environments, you must enable preview
+          environments on this cluster.
+        </Subheader>
         <ButtonEnablePREnvironments />
-      </>
+      </Placeholder>
     );
   }
 
@@ -249,12 +307,6 @@ const EnvironmentList = () => {
             Configure
           </SettingsButton>
         </ActionsWrapper>
-        {/* <Settings >
-          <SettingsIcon src={settings} />
-          <SettingsText>
-            Configure
-          </SettingsText>
-        </Settings> */}
       </ControlRow>
       <EventsGrid>{renderDeploymentList()}</EventsGrid>
     </Container>
@@ -308,23 +360,24 @@ const SettingsButton = styled.div`
 `;
 
 const Placeholder = styled.div`
-  width: 100%;
-  display: flex;
-  justify-content: center;
-  align-items: center;
+  padding: 30px;
+  margin-top: 35px;
+  padding-bottom: 40px;
+  font-size: 13px;
   color: #ffffff44;
-  background: #26282f;
-  border-radius: 5px;
-  height: 370px;
+  min-height: 400px;
+  height: 50vh;
+  background: #ffffff11;
+  border-radius: 8px;
+  width: 100%;
   display: flex;
   align-items: center;
   justify-content: center;
-  color: #ffffff44;
-  font-size: 13px;
+  flex-direction: column;
 
   > i {
-    font-size: 16px;
-    margin-right: 12px;
+    font-size: 18px;
+    margin-right: 8px;
   }
 `;
 
@@ -408,3 +461,15 @@ const StyledStatusSelector = styled.div`
   align-items: center;
   font-size: 13px;
 `;
+
+const Header = styled.div`
+  font-weight: 500;
+  color: #aaaabb;
+  font-size: 16px;
+  margin-bottom: 15px;
+  width: 50%;
+`;
+
+const Subheader = styled.div`
+  width: 50%;
+`;

+ 8 - 10
dashboard/src/main/home/cluster-dashboard/dashboard/preview-environments/components/ButtonEnablePREnvironments.tsx

@@ -86,7 +86,7 @@ const ButtonEnablePREnvironments = () => {
       <Container>
         <Button {...getButtonProps()}>
           <img src={pr_icon} alt="Pull request icon" />
-          Enable PR environments
+          Enable Preview Environments
         </Button>
       </Container>
     </>
@@ -98,18 +98,18 @@ export default ButtonEnablePREnvironments;
 const Button = styled(DynamicLink)`
   background-color: #616feecc;
   border: none;
-  border-radius: 15px;
+  border-radius: 6px;
   color: white;
   display: flex;
   align-items: center;
   justify-content: center;
-  padding: 10px 20px;
-  font-size: 16px;
+  padding: 8px 12px;
+  font-size: 14px;
   cursor: pointer;
   img {
     margin-right: 10px;
-    width: 30px;
-    height: 30px;
+    width: 20px;
+    height: 20px;
   }
   transition: background-color 150ms ease-out;
   :hover {
@@ -118,9 +118,7 @@ const Button = styled(DynamicLink)`
 `;
 
 const Container = styled.div`
-  width: 100%;
-  min-height: 400px;
+  width: 50%;
   display: flex;
-  align-items: center;
-  justify-content: center;
+  margin-top: 20px;
 `;

+ 11 - 0
dashboard/src/shared/api.tsx

@@ -644,6 +644,16 @@ const getGitRepoList = baseApi<
   return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id}/repos`;
 });
 
+const getGitRepoPermission = baseApi<
+  {},
+  {
+    project_id: number;
+    git_repo_id: number;
+  }
+>("GET", (pathParams) => {
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id}/permissions`;
+});
+
 const getGitRepos = baseApi<
   {},
   {
@@ -1371,6 +1381,7 @@ export default {
   getPRDeployment,
   getGHAWorkflowTemplate,
   getGitRepoList,
+  getGitRepoPermission,
   getGitRepos,
   getImageRepos,
   getImageTags,