# Project Pause Plugin

A OpenShift Console plugin that allows users to pause and resume projects (namespaces) through the web interface. This plugin integrates with the project-pause-operator to manage project state.

## Features

- Pause/Resume projects from the OpenShift Console
- View project pause status in project overview
- Track when projects were paused
- Integration with project-pause-operator

## Development

### Prerequisites

- [Node.js](https://nodejs.org/en/)
- [yarn](https://yarnpkg.com)
- [Docker](https://www.docker.com) or [podman 3.2.0+](https://podman.io)
- [oc](https://console.redhat.com/openshift/downloads)

### Local Development

In one terminal window:
```bash
yarn install
yarn run start
```

In another terminal window:
```bash
oc login  # login to your cluster
yarn run start-console
```

The plugin HTTP server runs on port 9001. Navigate to http://localhost:9000/example to see the running plugin.

### Building the Image

1. Build:
```bash
docker build -t quay.io/my-repository/project-pause-plugin:latest .
```

2. Push:
```bash
docker push quay.io/my-repository/project-pause-plugin:latest
```

Note: For Apple Silicon Macs, add `--platform=linux/amd64` when building.

### Deployment

Deploy using Helm:
```bash
helm upgrade -i project-pause-plugin charts/openshift-console-plugin \
  -n plugin__project-pause-plugin --create-namespace \
  --set plugin.image=quay.io/my-repository/project-pause-plugin:latest
```

## Required Operator Integration

The plugin requires a project-pause-operator that implements the following REST endpoints:

### 1. Get Project State
```
GET /api/proxy/project-pause-operator/v1/namespaces/{namespace}/state

Response:
{
  "isPaused": boolean,
  "pausedSince": string (ISO 8601 timestamp),
  "error": string (optional)
}
```

### 2. Toggle Project State
```
POST /api/proxy/project-pause-operator/v1/namespaces/{namespace}/toggle

Request:
{
  "pause": boolean
}

Response:
{
  "success": boolean,
  "error": string (optional)
}
```

### CRD Annotations

The operator should manage these annotations on namespaces:
```yaml
apiVersion: v1
kind: Namespace
metadata:
  annotations:
    project-pause-operator.openshift.io/paused: "true"
    project-pause-operator.openshift.io/paused-since: "2024-03-19T10:30:00Z"
    project-pause-operator.openshift.io/original-quota: '{"hard":{"cpu":"4","memory":"16Gi"}}'
```

### Example Operator Implementation

```go
const (
    PausedAnnotation = "project-pause-operator.openshift.io/paused"
    PausedSinceAnnotation = "project-pause-operator.openshift.io/paused-since"
)

type ProjectState struct {
    IsPaused    bool   `json:"isPaused"`
    PausedSince string `json:"pausedSince,omitempty"`
    Error       string `json:"error,omitempty"`
}

// GetProjectState handles the GET state endpoint
func (r *ProjectPauseReconciler) GetProjectState(w http.ResponseWriter, req *http.Request) {
    namespace := mux.Vars(req)["namespace"]
    
    ns, err := r.KubeClient.CoreV1().Namespaces().Get(context.Background(), namespace, metav1.GetOptions{})
    if err != nil {
        respondWithError(w, http.StatusNotFound, "namespace not found")
        return
    }
    
    state := ProjectState{
        IsPaused: ns.Annotations[PausedAnnotation] == "true",
    }
    
    if state.IsPaused {
        state.PausedSince = ns.Annotations[PausedSinceAnnotation]
    }
    
    json.NewEncoder(w).Encode(state)
}

// See full example in operator repository
```

## Plugin Architecture

The plugin provides:
1. Example test page at `/example`
2. Project overview integration using `console.project-overview/inventory-item`

### i18n Support

The plugin uses the namespace `plugin__project-pause-plugin` for translations. Update messages by running:
```bash
yarn i18n
```

### Security Considerations

The operator must validate that users have appropriate permissions before allowing pause/resume actions:

1. Use SubjectAccessReview to verify the user has `update` permission on the namespace
2. Ensure the request is authenticated using the user's token
3. Validate the namespace matches the one in the URL path

Example security implementation:
```go
// validateAccess checks if the user has permission to modify the namespace
func (r *ProjectPauseReconciler) validateAccess(req *http.Request, namespace string) error {
    // Get user info from request context
    userInfo, err := getUserInfo(req)
    if err != nil {
        return fmt.Errorf("failed to get user info: %v", err)
    }

    // Create SubjectAccessReview
    sar := &authorizationv1.SubjectAccessReview{
        Spec: authorizationv1.SubjectAccessReviewSpec{
            User: userInfo.Username,
            Groups: userInfo.Groups,
            ResourceAttributes: &authorizationv1.ResourceAttributes{
                Namespace: namespace,
                Verb: "update",
                Resource: "namespaces",
                Group: "",
            },
        },
    }

    // Check authorization
    result, err := r.KubeClient.AuthorizationV1().SubjectAccessReviews().Create(
        context.Background(), sar, metav1.CreateOptions{})
    if err != nil {
        return fmt.Errorf("failed to check authorization: %v", err)
    }

    if !result.Status.Allowed {
        return fmt.Errorf("user not authorized to modify namespace %s", namespace)
    }

    return nil
}

// Example usage in handler
func (r *ProjectPauseReconciler) ToggleProjectState(w http.ResponseWriter, req *http.Request) {
    namespace := mux.Vars(req)["namespace"]
    
    // Validate user has permission
    if err := r.validateAccess(req, namespace); err != nil {
        respondWithError(w, http.StatusForbidden, err.Error())
        return
    }
    
    // Continue with toggle operation...
}
```

### Quota Management

The operator manages resource quotas when pausing/resuming projects:

```go
type QuotaBackup struct {
    Hard v1.ResourceList `json:"hard"`
}

// pauseNamespace handles the actual pause operation
func (r *ProjectPauseReconciler) pauseNamespace(namespace string) error {
    // Get current quota
    quota, err := r.KubeClient.CoreV1().ResourceQuotas(namespace).Get(
        context.Background(), 
        "compute-resources", // or your quota name
        metav1.GetOptions{},
    )
    if err != nil && !errors.IsNotFound(err) {
        return fmt.Errorf("failed to get quota: %v", err)
    }

    // Backup current quota to annotation
    if quota != nil {
        backup := QuotaBackup{
            Hard: quota.Spec.Hard,
        }
        backupJSON, err := json.Marshal(backup)
        if err != nil {
            return fmt.Errorf("failed to marshal quota backup: %v", err)
        }

        ns, err := r.KubeClient.CoreV1().Namespaces().Get(
            context.Background(), 
            namespace, 
            metav1.GetOptions{},
        )
        if err != nil {
            return fmt.Errorf("failed to get namespace: %v", err)
        }

        if ns.Annotations == nil {
            ns.Annotations = make(map[string]string)
        }
        ns.Annotations["project-pause-operator.openshift.io/original-quota"] = string(backupJSON)

        _, err = r.KubeClient.CoreV1().Namespaces().Update(
            context.Background(), 
            ns, 
            metav1.UpdateOptions{},
        )
        if err != nil {
            return fmt.Errorf("failed to backup quota: %v", err)
        }

        // Set quota to zero
        quota.Spec.Hard = v1.ResourceList{
            v1.ResourceCPU:    resource.MustParse("0"),
            v1.ResourceMemory: resource.MustParse("0"),
        }
        
        _, err = r.KubeClient.CoreV1().ResourceQuotas(namespace).Update(
            context.Background(),
            quota,
            metav1.UpdateOptions{},
        )
        if err != nil {
            return fmt.Errorf("failed to update quota: %v", err)
        }
    }

    // Delete all pods
    err = r.KubeClient.CoreV1().Pods(namespace).DeleteCollection(
        context.Background(),
        metav1.DeleteOptions{},
        metav1.ListOptions{},
    )
    if err != nil {
        return fmt.Errorf("failed to delete pods: %v", err)
    }

    return nil
}

// resumeNamespace restores the original quota
func (r *ProjectPauseReconciler) resumeNamespace(namespace string) error {
    ns, err := r.KubeClient.CoreV1().Namespaces().Get(
        context.Background(),
        namespace,
        metav1.GetOptions{},
    )
    if err != nil {
        return fmt.Errorf("failed to get namespace: %v", err)
    }

    // Restore quota from backup
    quotaJSON := ns.Annotations["project-pause-operator.openshift.io/original-quota"]
    if quotaJSON != "" {
        var backup QuotaBackup
        if err := json.Unmarshal([]byte(quotaJSON), &backup); err != nil {
            return fmt.Errorf("failed to unmarshal quota backup: %v", err)
        }

        quota, err := r.KubeClient.CoreV1().ResourceQuotas(namespace).Get(
            context.Background(),
            "compute-resources", // or your quota name
            metav1.GetOptions{},
        )
        if err != nil {
            if !errors.IsNotFound(err) {
                return fmt.Errorf("failed to get quota: %v", err)
            }
            // Create new quota if it doesn't exist
            quota = &v1.ResourceQuota{
                ObjectMeta: metav1.ObjectMeta{
                    Name:      "compute-resources",
                    Namespace: namespace,
                },
            }
        }

        quota.Spec.Hard = backup.Hard
        
        if quota.ResourceVersion == "" {
            _, err = r.KubeClient.CoreV1().ResourceQuotas(namespace).Create(
                context.Background(),
                quota,
                metav1.CreateOptions{},
            )
        } else {
            _, err = r.KubeClient.CoreV1().ResourceQuotas(namespace).Update(
                context.Background(),
                quota,
                metav1.UpdateOptions{},
            )
        }
        if err != nil {
            return fmt.Errorf("failed to restore quota: %v", err)
        }
    }

    return nil
}
