ソースを参照

POR-1902 validate name against existing porter apps (#3774)

ianedwards 2 年 前
コミット
32cd1395ab

+ 15 - 1
api/server/handlers/porter_app/validate.go

@@ -112,14 +112,28 @@ func (c *ValidatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		}
 	}
 
+	existingApps, err := c.Repo().PorterApp().ListPorterAppsByProjectID(project.ID)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error listing porter apps by project id")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
 	if appProto.Name == "" {
 		err := telemetry.Error(ctx, span, nil, "app proto name is empty")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		return
 	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-name", Value: appProto.Name})
 
+	for _, existingApp := range existingApps {
+		if existingApp.Name == appProto.Name {
+			err := telemetry.Error(ctx, span, nil, "app with the provided name already exists in the project")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
+	}
 	telemetry.WithAttributes(span,
-		telemetry.AttributeKV{Key: "app-name", Value: appProto.Name},
 		telemetry.AttributeKV{Key: "deployment-target-id", Value: request.DeploymentTargetId},
 		telemetry.AttributeKV{Key: "commit-sha", Value: request.CommitSHA},
 	)

+ 7 - 1
dashboard/src/lib/porter-apps/index.ts

@@ -59,7 +59,13 @@ export const deletionValidator = z.object({
 export const clientAppValidator = z.object({
   name: z.object({
     readOnly: z.boolean(),
-    value: z.string(),
+    value: z
+      .string()
+      .min(1, { message: "Name must be at least 1 character" })
+      .max(30, { message: "Name must be 30 characters or less" })
+      .regex(/^[a-z0-9-]{1,61}$/, {
+        message: 'Lowercase letters, numbers, and "-" only.',
+      }),
   }),
   envGroups: z
     .object({ name: z.string(), version: z.bigint() })

+ 21 - 7
dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx

@@ -80,7 +80,7 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
   const { maxCPU, maxRAM } = useClusterResourceLimits({
     projectId: currentProject?.id,
     clusterId: currentCluster?.id,
-  })
+  });
 
   const { data: porterApps = [] } = useQuery<string[]>(
     ["getPorterApps", currentProject?.id, currentCluster?.id],
@@ -513,7 +513,7 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
                       placeholder="ex: academic-sophon"
                       type="text"
                       width="300px"
-                      error={errors.app?.name?.message}
+                      error={errors.app?.name?.value?.message}
                       disabled={name.readOnly}
                       disabledTooltip={
                         "You may only edit this field in your porter.yaml."
@@ -585,10 +585,23 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
                           <ImageSettings
                             projectId={currentProject.id}
                             imageUri={image?.repository ?? ""}
-                            setImageUri={(uri: string) => setValue("source.image", { ...image, repository: uri })}
+                            setImageUri={(uri: string) =>
+                              setValue("source.image", {
+                                ...image,
+                                repository: uri,
+                              })
+                            }
                             imageTag={image?.tag ?? ""}
-                            setImageTag={(tag: string) => setValue("source.image", { ...image, tag })}
-                            resetImageInfo={() => setValue("source.image", { ...image, repository: "", tag: "" })}
+                            setImageTag={(tag: string) =>
+                              setValue("source.image", { ...image, tag })
+                            }
+                            resetImageInfo={() =>
+                              setValue("source.image", {
+                                ...image,
+                                repository: "",
+                                tag: "",
+                              })
+                            }
                           />
                         )
                       ) : null}
@@ -614,8 +627,9 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
                             }
                           >
                             {detectedServices.count > 0
-                              ? `Detected ${detectedServices.count} service${detectedServices.count > 1 ? "s" : ""
-                              } from porter.yaml.`
+                              ? `Detected ${detectedServices.count} service${
+                                  detectedServices.count > 1 ? "s" : ""
+                                } from porter.yaml.`
                               : `Could not detect any services from porter.yaml. Make sure it exists in the root of your repo.`}
                           </Text>
                         </AppearingDiv>

+ 11 - 0
internal/repository/gorm/porter_app.go

@@ -34,6 +34,17 @@ func (repo *PorterAppRepository) ListPorterAppByClusterID(clusterID uint) ([]*mo
 	return apps, nil
 }
 
+// ListPorterAppsByProjectID returns a list of PorterApps by project ID.
+func (repo *PorterAppRepository) ListPorterAppsByProjectID(projectID uint) ([]*models.PorterApp, error) {
+	apps := []*models.PorterApp{}
+
+	if err := repo.db.Where("project_id = ?", projectID).Find(&apps).Error; err != nil {
+		return nil, err
+	}
+
+	return apps, nil
+}
+
 func (repo *PorterAppRepository) ReadPorterAppByName(clusterID uint, name string) (*models.PorterApp, error) {
 	app := &models.PorterApp{}
 

+ 1 - 0
internal/repository/porter_app.go

@@ -10,6 +10,7 @@ type PorterAppRepository interface {
 	ReadPorterAppsByProjectIDAndName(projectID uint, name string) ([]*models.PorterApp, error)
 	CreatePorterApp(app *models.PorterApp) (*models.PorterApp, error)
 	ListPorterAppByClusterID(clusterID uint) ([]*models.PorterApp, error)
+	ListPorterAppsByProjectID(projectID uint) ([]*models.PorterApp, error)
 	UpdatePorterApp(app *models.PorterApp) (*models.PorterApp, error)
 	DeletePorterApp(app *models.PorterApp) (*models.PorterApp, error)
 }

+ 6 - 0
internal/repository/test/porter_app.go

@@ -34,10 +34,16 @@ func (repo *PorterAppRepository) UpdatePorterApp(app *models.PorterApp) (*models
 	return nil, errors.New("cannot write database")
 }
 
+// ListPorterAppByClusterID is a test method that is not implemented
 func (repo *PorterAppRepository) ListPorterAppByClusterID(clusterID uint) ([]*models.PorterApp, error) {
 	return nil, errors.New("cannot write database")
 }
 
+// ListPorterAppsByProjectID is a test method that is not implemented
+func (repo *PorterAppRepository) ListPorterAppsByProjectID(projectID uint) ([]*models.PorterApp, error) {
+	return nil, errors.New("cannot write database")
+}
+
 func (repo *PorterAppRepository) DeletePorterApp(app *models.PorterApp) (*models.PorterApp, error) {
 	return nil, errors.New("cannot write database")
 }