├── .gitignore ├── src ├── rollout │ ├── templates │ │ ├── index.ts │ │ └── rollout-yaml.ts │ ├── components │ │ ├── AnalysisRunStatus │ │ │ └── AnalysisRunStatus.scss │ │ ├── Revisions │ │ │ ├── Revisions.scss │ │ │ └── RevisionsRowActions.tsx │ │ ├── RolloutPodsTab.tsx │ │ ├── RolloutRevisionsTab.tsx │ │ ├── Strategy │ │ │ ├── CanaryServices.tsx │ │ │ └── BlueGreenServices.tsx │ │ ├── RolloutStatus.tsx │ │ ├── RolloutNavPage.tsx │ │ └── RolloutDetailsTab.tsx │ ├── utils │ │ └── rollout-utils.ts │ ├── models │ │ ├── AnalysisRunModel.ts │ │ └── RolloutModel.ts │ └── services │ │ └── Rollout.ts ├── gitops │ ├── components │ │ ├── project │ │ │ ├── templates │ │ │ │ ├── index.ts │ │ │ │ └── appproject-yaml.ts │ │ │ ├── ProjectAppsTab.tsx │ │ │ ├── ResourceAllowDenyList.tsx │ │ │ ├── ProjectDetailsTab.tsx │ │ │ ├── DestinationsList.tsx │ │ │ ├── ProjectNavPage.tsx │ │ │ ├── hooks │ │ │ │ └── useProjectActionsProvider.tsx │ │ │ ├── ProjectRolesTab.tsx │ │ │ └── ProjectWindowsTab.tsx │ │ ├── application │ │ │ ├── templates │ │ │ │ ├── index.ts │ │ │ │ └── application-yaml.ts │ │ │ ├── History │ │ │ │ ├── History.scss │ │ │ │ └── History.tsx │ │ │ ├── Sources │ │ │ │ ├── Sources.scss │ │ │ │ └── Sources.tsx │ │ │ ├── ApplicationListTab.tsx │ │ │ ├── ApplicationHistoryTab.tsx │ │ │ ├── Revision │ │ │ │ └── Revision.tsx │ │ │ ├── Statuses │ │ │ │ ├── SyncStatus.tsx │ │ │ │ ├── HealthStatus.tsx │ │ │ │ └── OperationState.tsx │ │ │ ├── ApplicationNavPage.tsx │ │ │ ├── Conditions │ │ │ │ └── ConditionsPopover.tsx │ │ │ └── ResourceRowActions.tsx │ │ ├── dashboards │ │ │ ├── dashboardUtils.ts │ │ │ ├── ApplicationSets.tsx │ │ │ └── Applications.tsx │ │ ├── appset │ │ │ ├── generators │ │ │ │ ├── GenericGenerator.tsx │ │ │ │ ├── UnionGenerator.tsx │ │ │ │ ├── MatrixGenerator.tsx │ │ │ │ ├── MergeGenerator.tsx │ │ │ │ ├── GeneratorView.tsx │ │ │ │ ├── ValuesView.tsx │ │ │ │ ├── GitGenerator.tsx │ │ │ │ ├── ClusterGenerator.tsx │ │ │ │ └── ListGenerator.tsx │ │ │ ├── AppsTab.tsx │ │ │ ├── Status.tsx │ │ │ ├── GeneratorsTab.tsx │ │ │ ├── AppSetNavPage.tsx │ │ │ ├── Generators.tsx │ │ │ ├── AppSetDetailsTab.tsx │ │ │ └── hooks │ │ │ │ └── useAppSetActionsProvider.tsx │ │ └── shared │ │ │ └── ExternalLink.tsx │ ├── utils │ │ ├── constants.ts │ │ ├── urls.ts │ │ └── utils.ts │ ├── models │ │ ├── AppProjectModel.ts │ │ ├── ApplicationSetModel.ts │ │ └── ApplicationModel.ts │ └── services │ │ └── ArgoCD.ts ├── images │ ├── argo.png │ ├── git.png │ ├── helm.png │ └── kustomize.png ├── utils │ ├── components │ │ ├── ActionDropDownItem │ │ │ ├── action-dropdown-item.scss │ │ │ └── ActionDropDownItem.tsx │ │ ├── toggles │ │ │ ├── DropDownToggle.tsx │ │ │ ├── KebabToggle.tsx │ │ │ └── SelectToggle.tsx │ │ ├── ResourceYAMLTab │ │ │ └── ResourceYAMLTab.tsx │ │ ├── StandardDetailsGroup │ │ │ ├── MetadataLabels.tsx │ │ │ └── StandardDetailsGroup.tsx │ │ ├── EventsTab │ │ │ └── EventsTab.tsx │ │ ├── DetailsDescriptionGroup │ │ │ └── DetailsDescriptionGroup.tsx │ │ ├── ActionDropDown │ │ │ └── ActionDropDown.tsx │ │ ├── PageTitle │ │ │ └── PageTitle.tsx │ │ ├── ModalProvider │ │ │ └── ModalProvider.tsx │ │ ├── PodList │ │ │ └── PodRowActions.tsx │ │ ├── Conditions │ │ │ └── conditions.tsx │ │ └── ResourceDeleteModal │ │ │ └── ResourceDeleteModal.tsx │ └── hooks │ │ └── useGitOpsTranslation.ts └── externalsecrets │ ├── components │ ├── ESStatus.tsx │ ├── ESNavPage.tsx │ ├── hooks │ │ └── useESActionsProvider.tsx │ └── ESDetailsTab.tsx │ ├── models │ └── ExternalSecrets.ts │ └── utils │ └── es-utils.ts ├── OWNERS ├── ct.yaml ├── docs └── img │ ├── apps-list.png │ ├── apps-details.png │ ├── appset-list.png │ ├── appset-details.png │ ├── projects-list.png │ ├── rollouts-list.png │ ├── gitops-inventory.png │ ├── projects-details.png │ ├── rollouts-details.png │ ├── dashboard-inventory.png │ ├── rollouts-revisions.png │ ├── externalsecrets-list.png │ └── externalsecrets-details.png ├── manifests ├── base │ ├── namespace.yaml │ ├── kustomization.yaml │ ├── proxy-svc.yaml │ ├── plugin-svc.yaml │ ├── console-plugin.yaml │ ├── configmap.yaml │ ├── proxy-deploy.yaml │ └── plugin-deploy.yaml └── overlays │ └── install │ ├── README.md │ ├── plugin-patcher-sa.yaml │ ├── kustomization.yaml │ ├── plugin-patcher-cluster-role.yaml │ ├── plugin-patcher-cluster-rolebinding.yaml │ └── plugin-patcher-job.yaml ├── test ├── apps │ ├── long │ │ ├── manifests │ │ │ ├── kustomization.yaml │ │ │ └── long-job.yaml │ │ └── long-app.yaml │ ├── bad │ │ ├── bad-cr │ │ │ └── bad-cr.yaml │ │ └── error-app.yaml │ └── helm │ │ └── helm-app.yaml ├── eso │ └── eso-test.yaml └── appset │ ├── clusters │ └── clusters.yaml │ └── matrix │ ├── list-and-git.yaml │ └── union-matrix.yaml ├── integration-tests ├── fixtures │ └── example.json ├── support │ ├── index.ts │ └── login.ts ├── tsconfig.json ├── reporter-config.json ├── .eslintrc ├── cypress.config.js ├── plugins │ └── index.ts └── tests │ └── example-page.cy.ts ├── install_helm.sh ├── i18n-scripts ├── build-i18n.sh ├── lexers.js ├── common.js └── set-english-defaults.js ├── Dockerfile ├── test-frontend.sh ├── tsconfig.json ├── i18next-parser.config.js ├── test-prow-e2e.sh ├── locales └── en │ └── plugin__console-plugin-template.json ├── plugin-metadata.ts ├── start-console.sh └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | dist -------------------------------------------------------------------------------- /src/rollout/templates/index.ts: -------------------------------------------------------------------------------- 1 | export * from './rollout-yaml'; 2 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | approvers: 2 | - gnunn1 3 | component: OpenShift GitOps Admin Plugin 4 | -------------------------------------------------------------------------------- /src/gitops/components/project/templates/index.ts: -------------------------------------------------------------------------------- 1 | export * from './appproject-yaml'; 2 | -------------------------------------------------------------------------------- /src/gitops/components/application/templates/index.ts: -------------------------------------------------------------------------------- 1 | export * from './application-yaml'; 2 | -------------------------------------------------------------------------------- /src/images/argo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnunn-gitops/gitops-admin-plugin/HEAD/src/images/argo.png -------------------------------------------------------------------------------- /src/images/git.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnunn-gitops/gitops-admin-plugin/HEAD/src/images/git.png -------------------------------------------------------------------------------- /src/images/helm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnunn-gitops/gitops-admin-plugin/HEAD/src/images/helm.png -------------------------------------------------------------------------------- /ct.yaml: -------------------------------------------------------------------------------- 1 | chart-dirs: 2 | - charts 3 | validate-maintainers: false 4 | remote: origin 5 | target-branch: main 6 | -------------------------------------------------------------------------------- /docs/img/apps-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnunn-gitops/gitops-admin-plugin/HEAD/docs/img/apps-list.png -------------------------------------------------------------------------------- /manifests/base/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: gitops-admin-plugin -------------------------------------------------------------------------------- /docs/img/apps-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnunn-gitops/gitops-admin-plugin/HEAD/docs/img/apps-details.png -------------------------------------------------------------------------------- /docs/img/appset-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnunn-gitops/gitops-admin-plugin/HEAD/docs/img/appset-list.png -------------------------------------------------------------------------------- /src/images/kustomize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnunn-gitops/gitops-admin-plugin/HEAD/src/images/kustomize.png -------------------------------------------------------------------------------- /docs/img/appset-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnunn-gitops/gitops-admin-plugin/HEAD/docs/img/appset-details.png -------------------------------------------------------------------------------- /docs/img/projects-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnunn-gitops/gitops-admin-plugin/HEAD/docs/img/projects-list.png -------------------------------------------------------------------------------- /docs/img/rollouts-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnunn-gitops/gitops-admin-plugin/HEAD/docs/img/rollouts-list.png -------------------------------------------------------------------------------- /docs/img/gitops-inventory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnunn-gitops/gitops-admin-plugin/HEAD/docs/img/gitops-inventory.png -------------------------------------------------------------------------------- /docs/img/projects-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnunn-gitops/gitops-admin-plugin/HEAD/docs/img/projects-details.png -------------------------------------------------------------------------------- /docs/img/rollouts-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnunn-gitops/gitops-admin-plugin/HEAD/docs/img/rollouts-details.png -------------------------------------------------------------------------------- /docs/img/dashboard-inventory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnunn-gitops/gitops-admin-plugin/HEAD/docs/img/dashboard-inventory.png -------------------------------------------------------------------------------- /docs/img/rollouts-revisions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnunn-gitops/gitops-admin-plugin/HEAD/docs/img/rollouts-revisions.png -------------------------------------------------------------------------------- /manifests/overlays/install/README.md: -------------------------------------------------------------------------------- 1 | Includes a job to patch the plugin into the console, useful when using Argo CD to deploy this. -------------------------------------------------------------------------------- /docs/img/externalsecrets-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnunn-gitops/gitops-admin-plugin/HEAD/docs/img/externalsecrets-list.png -------------------------------------------------------------------------------- /docs/img/externalsecrets-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnunn-gitops/gitops-admin-plugin/HEAD/docs/img/externalsecrets-details.png -------------------------------------------------------------------------------- /test/apps/long/manifests/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: demo 2 | 3 | resources: 4 | #- github.com/gitops-examples/simple-app-example 5 | - long-job.yaml 6 | -------------------------------------------------------------------------------- /test/apps/bad/bad-cr/bad-cr.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: fake.redhat.com/v1 2 | kind: FakeCustomResource 3 | metadata: 4 | name: fake 5 | namespace: openshift-gitops 6 | spec: 7 | somevalue: fake 8 | -------------------------------------------------------------------------------- /src/utils/components/ActionDropDownItem/action-dropdown-item.scss: -------------------------------------------------------------------------------- 1 | .ActionDropdownItem { 2 | &__disabled { 3 | cursor: unset; 4 | color: var(--pf-global--disabled-color--100); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /integration-tests/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /src/gitops/components/application/History/History.scss: -------------------------------------------------------------------------------- 1 | .gitops-admin-plugin__history-id-column { 2 | --pf-c-table--cell--Width: 140px; 3 | --pf-c-table--cell--MinWidth: 140px; 4 | --pf-c-table--cell--MaxWidth: 300px 5 | } -------------------------------------------------------------------------------- /src/gitops/components/application/Sources/Sources.scss: -------------------------------------------------------------------------------- 1 | .gitops-admin-plugin__sources-type-column { 2 | --pf-c-table--cell--Width: 100px; 3 | --pf-c-table--cell--MinWidth: 100px; 4 | --pf-c-table--cell--MaxWidth: 200px 5 | } -------------------------------------------------------------------------------- /integration-tests/support/index.ts: -------------------------------------------------------------------------------- 1 | // Import commands.js using ES2015 syntax: 2 | import './login'; 3 | 4 | export const checkErrors = () => 5 | cy.window().then((win) => { 6 | assert.isTrue(!win.windowError, win.windowError); 7 | }); 8 | -------------------------------------------------------------------------------- /install_helm.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | USE_SUDO="false" 4 | HELM_INSTALL_DIR="/tmp" 5 | 6 | curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 7 | chmod 700 get_helm.sh 8 | source get_helm.sh 9 | 10 | rm -rf get_helm.sh -------------------------------------------------------------------------------- /integration-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "types":["cypress","node"], 6 | "isolatedModules": false 7 | }, 8 | "include": ["../node_modules/cypress", "./**/*.ts"] 9 | } -------------------------------------------------------------------------------- /i18n-scripts/build-i18n.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -exuo pipefail 4 | 5 | FILE_PATTERN="{!(dist|node_modules)/**/*.{js,jsx,ts,tsx,json},*.{js,jsx,ts,tsx,json}}" 6 | 7 | i18next "${FILE_PATTERN}" [-oc] -c "./i18next-parser.config.js" -o "locales/\$LOCALE/\$NAMESPACE.json" 8 | -------------------------------------------------------------------------------- /manifests/overlays/install/plugin-patcher-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app: gitops-admin-plugin 6 | app.kubernetes.io/instance: gitops-admin-plugin 7 | app.kubernetes.io/name: gitops-admin-plugin 8 | app.kubernetes.io/part-of: gitops-admin-plugin 9 | name: gitops-admin-plugin-patcher -------------------------------------------------------------------------------- /src/rollout/components/AnalysisRunStatus/AnalysisRunStatus.scss: -------------------------------------------------------------------------------- 1 | .gitops_admin_plugin__measurement_border { 2 | border-radius: 8px; 3 | /* Todo: Replace with PF color */ 4 | border: 2px solid #D2D2D2; 5 | padding: 4px 8px 4px 8px; 6 | } 7 | 8 | .gitops_admin_plugin__tight_description_list { 9 | --pf-c-description-list--m-compact--RowGap: 0.2rem; 10 | } -------------------------------------------------------------------------------- /manifests/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: gitops-admin-plugin 2 | 3 | commonLabels: 4 | app.kubernetes.io/app: gitops-admin-plugin 5 | app.kubernetes.io/part-of: gitops-admin-plugin 6 | app.kubernetes.io/name: gitops-admin-plugin 7 | 8 | resources: 9 | - console-plugin.yaml 10 | - namespace.yaml 11 | - configmap.yaml 12 | - plugin-deploy.yaml 13 | - plugin-svc.yaml -------------------------------------------------------------------------------- /src/rollout/components/Revisions/Revisions.scss: -------------------------------------------------------------------------------- 1 | .gitops-admin-plugin__revision-column { 2 | --pf-c-table--cell--Width: 140px; 3 | --pf-c-table--cell--MinWidth: 140px; 4 | --pf-c-table--cell--MaxWidth: 300px 5 | } 6 | .gitops-admin-plugin__pods-column { 7 | --pf-c-table--cell--Width: 160px; 8 | --pf-c-table--cell--MinWidth: 160px; 9 | --pf-c-table--cell--MaxWidth: 300px 10 | } -------------------------------------------------------------------------------- /src/gitops/components/project/templates/appproject-yaml.ts: -------------------------------------------------------------------------------- 1 | export const defaultAppProjectYamlTemplate = ` 2 | apiVersion: "arjoproj/v1alpha1" 3 | kind: AppProject 4 | metadata: 5 | name: my-app-project 6 | spec: 7 | clusterResourceWhitelist: 8 | - group: '*' 9 | kind: '*' 10 | description: My App Project 11 | destinations: 12 | - namespace: '*' 13 | server: https://kubernetes.default.svc 14 | `; 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.access.redhat.com/ubi8/nodejs-16:latest AS build 2 | USER root 3 | RUN command -v yarn || npm i -g yarn 4 | 5 | ADD . /usr/src/app 6 | WORKDIR /usr/src/app 7 | RUN yarn install && yarn build 8 | 9 | FROM registry.access.redhat.com/ubi8/nginx-120:latest 10 | 11 | COPY --from=build /usr/src/app/dist /usr/share/nginx/html 12 | USER 1001 13 | 14 | ENTRYPOINT ["nginx", "-g", "daemon off;"] 15 | -------------------------------------------------------------------------------- /manifests/overlays/install/kustomization.yaml: -------------------------------------------------------------------------------- 1 | namespace: gitops-admin-plugin 2 | 3 | commonLabels: 4 | app.kubernetes.io/app: gitops-admin-plugin 5 | app.kubernetes.io/part-of: gitops-admin-plugin 6 | app.kubernetes.io/name: gitops-admin-plugin 7 | 8 | resources: 9 | - ../../base 10 | - plugin-patcher-sa.yaml 11 | - plugin-patcher-cluster-role.yaml 12 | - plugin-patcher-cluster-rolebinding.yaml 13 | - plugin-patcher-job.yaml 14 | -------------------------------------------------------------------------------- /test/apps/long/long-app.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: long 5 | namespace: gitops-basic-demo 6 | spec: 7 | destination: 8 | namespace: demo 9 | server: https://kubernetes.default.svc 10 | project: default 11 | source: 12 | path: test/apps/long/manifests 13 | repoURL: https://github.com/gnunn-gitops/gitops-admin-plugin.git 14 | targetRevision: HEAD 15 | syncPolicy: {} -------------------------------------------------------------------------------- /src/gitops/components/dashboards/dashboardUtils.ts: -------------------------------------------------------------------------------- 1 | export { ApplicationModel } from '@gitops-models/ApplicationModel'; 2 | export { ApplicationSetModel } from '@gitops-models/ApplicationSetModel'; 3 | 4 | // Supports OpenShift Console Dashboard inventory plugin 5 | export enum InventoryStatusGroup { 6 | WARN = "WARN", 7 | ERROR = "ERROR", 8 | PROGRESS = "PROGRESS", 9 | NOT_MAPPED = "NOT_MAPPED", 10 | UNKNOWN = "UNKNOWN" 11 | } 12 | -------------------------------------------------------------------------------- /test/apps/bad/error-app.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: error-app 5 | namespace: openshift-gitops 6 | spec: 7 | destination: 8 | namespace: demo 9 | server: https://kubernetes.default.svc 10 | project: default 11 | source: 12 | path: path/does/not/exist 13 | repoURL: https://github.com/gnunn-gitops/gitops-admin-plugin.git 14 | targetRevision: HEAD 15 | syncPolicy: {} 16 | -------------------------------------------------------------------------------- /integration-tests/reporter-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporterEnabled": "mocha-junit-reporter, mochawesome", 3 | "mochaJunitReporterReporterOptions": { 4 | "mochaFile": "./screenshots/junit_cypress-[hash].xml", 5 | "toConsole": false 6 | }, 7 | "mochawesomeReporterOptions": { 8 | "reportDir": "./screenshots/", 9 | "reportFilename": "cypress_report", 10 | "overwrite": false, 11 | "html": false, 12 | "json": true 13 | } 14 | } -------------------------------------------------------------------------------- /test-frontend.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # https://ci-operator-configresolver-ui-ci.apps.ci.l2s4.p1.openshiftapps.com/help#env 6 | OPENSHIFT_CI=${OPENSHIFT_CI:=false} 7 | ARTIFACT_DIR=${ARTIFACT_DIR:=/tmp/artifacts} 8 | 9 | yarn i18n 10 | GIT_STATUS="$(git status --short --untracked-files -- locales)" 11 | if [ -n "$GIT_STATUS" ]; then 12 | echo "i18n files are not up to date. Run 'yarn i18n' then commit changes." 13 | git --no-pager diff 14 | exit 1 15 | fi -------------------------------------------------------------------------------- /src/utils/components/toggles/DropDownToggle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Ref } from 'react'; 3 | 4 | import { MenuToggle, MenuToggleElement, MenuToggleProps } from '@patternfly/react-core'; 5 | 6 | const DropdownToggle = 7 | ({ children, ...props }: MenuToggleProps) => 8 | (toggleRef: Ref) => 9 | ( 10 | 11 | {children} 12 | 13 | ); 14 | 15 | export default DropdownToggle; 16 | -------------------------------------------------------------------------------- /src/utils/hooks/useGitOpsTranslation.ts: -------------------------------------------------------------------------------- 1 | import { getI18n, useTranslation } from 'react-i18next'; 2 | 3 | /** 4 | * A Hook for using the i18n translation. 5 | */ 6 | export const useGitOpsTranslation = () => useTranslation('plugin__gitops-admin-plugin'); 7 | 8 | /** 9 | * a function to perform translation to 'plugin__gitops-admin-plugin' namespace 10 | * @param value string to translate 11 | */ 12 | // skipcq: JS-C1002 13 | export const t = (value: string) => getI18n().t(value, { ns: 'plugin__gitops-admin-plugin' }); 14 | -------------------------------------------------------------------------------- /src/gitops/components/appset/generators/GenericGenerator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import GeneratorView from './GeneratorView'; 3 | 4 | interface GenericGeneratorProps { 5 | gentype: string, 6 | generator: Object 7 | } 8 | 9 | export const GenericGeneratorFragment: React.FC = ({ gentype, generator }) => { 10 | 11 | return ( 12 | 13 | This is an unknown type of Generator 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/components/toggles/KebabToggle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Ref } from 'react'; 3 | 4 | import { MenuToggle, MenuToggleElement, MenuToggleProps } from '@patternfly/react-core'; 5 | import { EllipsisVIcon } from '@patternfly/react-icons'; 6 | 7 | const KebabToggle = (props: MenuToggleProps) => (toggleRef: Ref) => 8 | ( 9 | 10 | 11 | 12 | ); 13 | 14 | export default KebabToggle; 15 | -------------------------------------------------------------------------------- /integration-tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "cypress/globals": true, 4 | "node": true 5 | }, 6 | "extends": ["../.eslintrc.yml", "plugin:cypress/recommended"], 7 | "plugins": ["cypress"], 8 | "rules": { 9 | "no-console": "off", 10 | "no-namespace": "off", 11 | "no-redeclare": "off", 12 | "promise/catch-or-return": "off", 13 | "promise/no-nesting": "off", 14 | "@typescript-eslint/no-var-requires":"off", 15 | "@typescript-eslint/no-namespace":"off" 16 | } 17 | } -------------------------------------------------------------------------------- /manifests/overlays/install/plugin-patcher-cluster-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app: gitops-admin-plugin 6 | app.kubernetes.io/instance: gitops-admin-plugin 7 | app.kubernetes.io/name: gitops-admin-plugin 8 | app.kubernetes.io/part-of: gitops-admin-plugin 9 | name: gitops-admin-plugin-patcher 10 | rules: 11 | - apiGroups: 12 | - operator.openshift.io 13 | resources: 14 | - consoles 15 | verbs: 16 | - get 17 | - list 18 | - patch 19 | - update 20 | -------------------------------------------------------------------------------- /src/gitops/components/application/templates/application-yaml.ts: -------------------------------------------------------------------------------- 1 | export const defaultApplicationYamlTemplate = ` 2 | apiVersion: "argoproj.io/v1alpha1" 3 | kind: "Application" 4 | metadata: 5 | name: bgd-app 6 | spec: 7 | destination: 8 | server: "https://kubernetes.default.svc" 9 | project: default 10 | source: 11 | path: documentation/modules/ROOT/examples/bgd 12 | repoURL: "https://github.com/OpenShiftDemos/openshift-gitops-workshop" 13 | targetRevision: master 14 | syncPolicy: 15 | automated: 16 | prune: true 17 | selfHeal: false 18 | `; 19 | -------------------------------------------------------------------------------- /src/utils/components/toggles/SelectToggle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Ref } from 'react'; 3 | 4 | import { MenuToggle, MenuToggleElement, MenuToggleProps } from '@patternfly/react-core'; 5 | 6 | type SelectToggleProps = MenuToggleProps & { 7 | selected: any; 8 | }; 9 | 10 | const SelectToggle = ({ selected, ...menuProps }: SelectToggleProps) => { 11 | return (toggleRef: Ref) => ( 12 | 13 | {selected} 14 | 15 | ); 16 | }; 17 | 18 | export default SelectToggle; 19 | -------------------------------------------------------------------------------- /test/apps/helm/helm-app.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: product-catalog 5 | namespace: gitops-basic-demo 6 | spec: 7 | project: default 8 | source: 9 | helm: 10 | releaseName: product-catalog 11 | parameters: 12 | - name: builds.enable 13 | value: "false" 14 | chart: product-catalog 15 | repoURL: https://gnunn-gitops.github.io/helm-charts 16 | targetRevision: 0.1.7 17 | destination: 18 | server: "https://kubernetes.default.svc" 19 | namespace: demo 20 | syncPolicy: {} 21 | -------------------------------------------------------------------------------- /test/eso/eso-test.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: external-secrets.io/v1beta1 2 | kind: ExternalSecret 3 | metadata: 4 | name: eso-test 5 | namespace: default 6 | spec: 7 | data: 8 | - remoteRef: 9 | conversionStrategy: Default 10 | decodingStrategy: None 11 | key: AWS_LETSENCRYPT_TYPO 12 | metadataPolicy: None 13 | secretKey: secret-access-key 14 | refreshInterval: 1h 15 | secretStoreRef: 16 | kind: ClusterSecretStore 17 | name: doppler-cluster 18 | target: 19 | creationPolicy: Owner 20 | deletionPolicy: Retain 21 | name: letsencrypt-aws 22 | -------------------------------------------------------------------------------- /src/gitops/components/shared/ExternalLink.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ReactNode } from 'react'; 3 | import { Text } from '@patternfly/react-core'; 4 | import { ExternalLinkAltIcon } from '@patternfly/react-icons'; 5 | 6 | 7 | type ExternalLinkProps = { 8 | href?: string; 9 | children?: ReactNode; 10 | }; 11 | 12 | const ExternalLink = ({ href, children }: ExternalLinkProps) => ( 13 | 14 | {children ? children : href} 15 | 16 | ); 17 | export default ExternalLink; 18 | -------------------------------------------------------------------------------- /src/utils/components/ResourceYAMLTab/ResourceYAMLTab.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { RouteComponentProps } from 'react-router'; 3 | 4 | import { K8sResourceCommon, ResourceYAMLEditor } from '@openshift-console/dynamic-plugin-sdk'; 5 | 6 | type ResourceYAMLTabProps = RouteComponentProps<{ 7 | ns: string; 8 | name: string; 9 | }> & { 10 | obj?: K8sResourceCommon; 11 | }; 12 | 13 | const ResourceYAMLTab: React.FC = ({ obj }) => { 14 | return ( 15 | 16 | ); 17 | }; 18 | 19 | export default ResourceYAMLTab; 20 | -------------------------------------------------------------------------------- /manifests/overlays/install/plugin-patcher-cluster-rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app: gitops-admin-plugin 6 | app.kubernetes.io/instance: gitops-admin-plugin 7 | app.kubernetes.io/name: gitops-admin-plugin 8 | app.kubernetes.io/part-of: gitops-admin-plugin 9 | name: gitops-admin-plugin-patcher 10 | roleRef: 11 | apiGroup: rbac.authorization.k8s.io 12 | kind: ClusterRole 13 | name: gitops-admin-plugin-patcher 14 | subjects: 15 | - kind: ServiceAccount 16 | name: gitops-admin-plugin-patcher 17 | namespace: gitops-admin-plugin 18 | -------------------------------------------------------------------------------- /manifests/base/proxy-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | annotations: 5 | service.alpha.openshift.io/serving-cert-signed-by: openshift-service-serving-signer@1685893963 6 | service.beta.openshift.io/serving-cert-secret-name: gitops-plugin-proxy-certs 7 | service.beta.openshift.io/serving-cert-signed-by: openshift-service-serving-signer@1685893963 8 | labels: 9 | app: gitops-plugin-proxy 10 | app.kubernetes.io/instance: gitops-plugin-proxy 11 | name: gitops-plugin-proxy 12 | spec: 13 | type: ClusterIP 14 | ports: 15 | - name: https 16 | port: 8443 17 | selector: 18 | name: gitops-plugin-proxy 19 | -------------------------------------------------------------------------------- /manifests/base/plugin-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | annotations: 5 | service.alpha.openshift.io/serving-cert-secret-name: gitops-admin-plugin-cert 6 | labels: 7 | app: gitops-admin-plugin 8 | app.kubernetes.io/instance: gitops-admin-plugin 9 | app.openshift.io/runtime: nodejs 10 | name: gitops-admin-plugin 11 | spec: 12 | type: ClusterIP 13 | ports: 14 | - name: 9443-tcp 15 | port: 9443 16 | selector: 17 | app: gitops-admin-plugin 18 | app.kubernetes.io/instance: gitops-admin-plugin 19 | app.kubernetes.io/name: gitops-admin-plugin 20 | app.kubernetes.io/part-of: gitops-admin-plugin 21 | -------------------------------------------------------------------------------- /test/appset/clusters/clusters.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: ApplicationSet 3 | metadata: 4 | name: guestbook 5 | spec: 6 | generators: 7 | - clusters: 8 | selector: 9 | matchLabels: 10 | staging: "true" 11 | values: 12 | revision: staging 13 | target: test 14 | template: 15 | metadata: 16 | name: '{{name}}-guestbook' 17 | spec: 18 | project: "default" 19 | source: 20 | repoURL: https://github.com/argoproj/argocd-example-apps/ 21 | targetRevision: HEAD 22 | path: guestbook 23 | destination: 24 | server: '{{server}}' 25 | namespace: guestbook 26 | -------------------------------------------------------------------------------- /src/gitops/components/application/ApplicationListTab.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import ApplicationListFragment from "@gitops-shared/ApplicationList"; 3 | 4 | type ApplicationListProps = { 5 | namespace: string; 6 | hideNameLabelFilters?: boolean; 7 | showTitle?: boolean; 8 | } 9 | 10 | const ApplicationListTab: React.FC = ({ namespace, hideNameLabelFilters, showTitle }) => { 11 | console.log(hideNameLabelFilters); 12 | return ( 13 | 18 | ) 19 | } 20 | 21 | export default ApplicationListTab; 22 | -------------------------------------------------------------------------------- /src/gitops/components/appset/AppsTab.tsx: -------------------------------------------------------------------------------- 1 | import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; 2 | import * as React from 'react'; 3 | import { RouteComponentProps } from 'react-router'; 4 | import ApplicationList from '@gitops-shared/ApplicationList'; 5 | 6 | type AppSetAppsProps = RouteComponentProps<{ 7 | ns: string; 8 | name: string; 9 | }> & { 10 | obj?: K8sResourceCommon; 11 | }; 12 | 13 | const AppSetAppsPage: React.FC = ({ obj }) => { 14 | 15 | return ( 16 | 20 | ) 21 | } 22 | 23 | export default AppSetAppsPage; 24 | -------------------------------------------------------------------------------- /manifests/base/console-plugin.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: console.openshift.io/v1 2 | kind: ConsolePlugin 3 | metadata: 4 | labels: 5 | app: gitops-admin-plugin 6 | name: gitops-admin-plugin 7 | spec: 8 | backend: 9 | service: 10 | basePath: / 11 | name: gitops-admin-plugin 12 | namespace: gitops-admin-plugin 13 | port: 9443 14 | type: Service 15 | displayName: GitOps Admin Plugin 16 | i18n: 17 | loadType: "" 18 | # proxy: 19 | # - alias: proxy 20 | # authorization: UserToken 21 | # endpoint: 22 | # service: 23 | # name: gitops-plugin-proxy 24 | # namespace: gitops-admin-plugin 25 | # port: 8443 26 | # type: Service 27 | -------------------------------------------------------------------------------- /src/gitops/components/project/ProjectAppsTab.tsx: -------------------------------------------------------------------------------- 1 | import { AppProjectKind } from '@gitops-models/AppProjectModel'; 2 | import * as React from 'react'; 3 | import { RouteComponentProps } from 'react-router'; 4 | import ApplicationListFragment from '@gitops-shared/ApplicationList'; 5 | 6 | type ProjectAppsProps = RouteComponentProps<{ 7 | ns: string; 8 | name: string; 9 | }> & { 10 | obj?: AppProjectKind; 11 | }; 12 | 13 | const ProjectAppsPage: React.FC = ({ obj }) => { 14 | 15 | return ( 16 | 20 | ) 21 | } 22 | 23 | export default ProjectAppsPage; 24 | -------------------------------------------------------------------------------- /manifests/base/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | labels: 5 | app: gitops-admin-plugin 6 | app.kubernetes.io/instance: gitops-admin-plugin 7 | name: gitops-admin-plugin 8 | data: 9 | nginx.conf: | 10 | error_log /dev/stdout info; 11 | events {} 12 | http { 13 | access_log /dev/stdout; 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | keepalive_timeout 65; 17 | server { 18 | listen 9443 ssl; 19 | ssl_certificate /var/cert/tls.crt; 20 | ssl_certificate_key /var/cert/tls.key; 21 | root /usr/share/nginx/html; 22 | } 23 | } -------------------------------------------------------------------------------- /src/rollout/utils/rollout-utils.ts: -------------------------------------------------------------------------------- 1 | import { RolloutKind } from "@rollout-models/RolloutModel"; 2 | 3 | export enum RolloutStatus { 4 | Progressing = 'Progressing', 5 | Degraded = 'Degraded', 6 | Paused = 'Paused', 7 | Healthy = 'Healthy', 8 | } 9 | 10 | export enum AnalysisRunStatus { 11 | Successful = 'Successful', 12 | Inconclusive = 'Inconclusive', 13 | Failed = 'Failed', 14 | Error = 'Error', 15 | Pending = 'Pending', 16 | Running = 'Running' 17 | } 18 | 19 | export function isDeploying(ro: RolloutKind) { 20 | if (ro?.status?.phase) { 21 | return ro.status?.phase === RolloutStatus.Progressing || ro.status?.phase === RolloutStatus.Paused; 22 | } else { 23 | return false; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /integration-tests/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress'); 2 | 3 | module.exports = defineConfig({ 4 | viewportWidth: 1920, 5 | viewportHeight: 1080, 6 | screenshotsFolder: './screenshots', 7 | videosFolder: './videos', 8 | video: true, 9 | reporter: '../../node_modules/cypress-multi-reporters', 10 | reporterOptions: { 11 | configFile: 'reporter-config.json', 12 | }, 13 | fixturesFolder: 'fixtures', 14 | defaultCommandTimeout: 30000, 15 | retries: { 16 | runMode: 1, 17 | openMode: 0, 18 | }, 19 | e2e: { 20 | setupNodeEvents(on, config) { 21 | return require('./plugins/index.ts')(on, config); 22 | }, 23 | specPattern: 'tests/**/*.cy.{js,jsx,ts,tsx}', 24 | supportFile: 'support/index.ts', 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /src/rollout/templates/rollout-yaml.ts: -------------------------------------------------------------------------------- 1 | export const defaultRolloutYamlTemplate = ` 2 | apiVersion: "argoproj.io/v1alpha1" 3 | kind: Rollout 4 | metadata: 5 | name: rollout-bluegreen 6 | spec: 7 | replicas: 2 8 | revisionHistoryLimit: 2 9 | selector: 10 | matchLabels: 11 | app: rollout-bluegreen 12 | template: 13 | metadata: 14 | labels: 15 | app: rollout-bluegreen 16 | spec: 17 | containers: 18 | - name: rollouts-demo 19 | image: argoproj/rollouts-demo:blue 20 | imagePullPolicy: Always 21 | ports: 22 | - containerPort: 8080 23 | strategy: 24 | blueGreen: 25 | activeService: rollout-bluegreen-active 26 | previewService: rollout-bluegreen-preview 27 | autoPromotionEnabled: false 28 | `; 29 | -------------------------------------------------------------------------------- /src/gitops/components/appset/generators/UnionGenerator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import GeneratorView from './GeneratorView'; 3 | import {UnityIcon} from '@patternfly/react-icons' 4 | import { UnionAppSetGenerator } from '@gitops-models/ApplicationSetModel'; 5 | import Generators from '../Generators'; 6 | 7 | interface UnionGeneratorProps { 8 | generator: UnionAppSetGenerator 9 | } 10 | 11 | export const UnionGeneratorFragment: React.FC = ({ generator }) => { 12 | return ( 13 | <> 14 | } title="Union"/> 15 |
16 |
17 | 18 |
19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/gitops/components/appset/generators/MatrixGenerator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import GeneratorView from './GeneratorView'; 3 | import {ThLargeIcon} from '@patternfly/react-icons' 4 | import { MatrixAppSetGenerator } from '@gitops-models/ApplicationSetModel'; 5 | import Generators from '../Generators'; 6 | 7 | interface MatrixGeneratorProps { 8 | generator: MatrixAppSetGenerator 9 | } 10 | 11 | export const MatrixGeneratorFragment: React.FC = ({ generator }) => { 12 | return ( 13 | <> 14 | } title="Matrix"/> 15 |
16 |
17 | 18 |
19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /integration-tests/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import * as wp from '@cypress/webpack-preprocessor'; 2 | 3 | module.exports = (on, config) => { 4 | const options = { 5 | webpackOptions: { 6 | resolve: { 7 | extensions: ['.ts', '.tsx', '.js'], 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.tsx?$/, 13 | loader: 'ts-loader', 14 | options: { happyPackMode: true, transpileOnly: true }, 15 | }, 16 | ], 17 | }, 18 | }, 19 | }; 20 | on('file:preprocessor', wp(options)); 21 | // `config` is the resolved Cypress config 22 | config.baseUrl = `${ 23 | process.env.BRIDGE_BASE_ADDRESS || 'http://localhost:9000/' 24 | }`; 25 | config.env.BRIDGE_KUBEADMIN_PASSWORD = process.env.BRIDGE_KUBEADMIN_PASSWORD; 26 | return config; 27 | }; 28 | -------------------------------------------------------------------------------- /src/gitops/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_NAMESPACE = 'default'; 2 | 3 | export enum SyncStatus { 4 | SYNCED = "Synced", 5 | SYNCED_FAILED = "SyncFailed", 6 | OUT_OF_SYNC = "OutOfSync", 7 | PRUNE_SKIPPED = "PruneSkipped", 8 | UNKNOWN = "Unknown" 9 | } 10 | 11 | export enum HealthStatus { 12 | HEALTHY = "Healthy", 13 | DEGRADED = "Degraded", 14 | SUSPENDED = "Suspended", 15 | MISSING = "Missing", 16 | PROGRESSING = "Progressing", 17 | UNKNOWN = "Unknown" 18 | } 19 | 20 | export enum PhaseStatus { 21 | TERMINATING = 'Terminating', 22 | RUNNING = 'Running', 23 | SUCCEEDED = 'Succeeded', 24 | FAILED = 'Failed', 25 | ERROR = 'Error' 26 | } 27 | 28 | export enum ApplicationSetStatus { 29 | HEALTHY = "Healthy", 30 | ERROR= "Error", 31 | UNKNOWN = "Unknown" 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/components/StandardDetailsGroup/MetadataLabels.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Label, LabelGroup } from '@patternfly/react-core'; 3 | 4 | type MetadataLabelsProps = { 5 | labels?: { [key: string]: string }; 6 | }; 7 | 8 | const MetadataLabels: React.FC = ({ labels }) => { 9 | return ( 10 | (labels && Object.keys(labels).length > 0) ? 11 | ( 12 | 13 | {Object.keys(labels || {})?.map((key) => { 14 | return ; 15 | })} 16 | 17 | ) : 18 | ( 19 | No labels 20 | ) 21 | ); 22 | 23 | }; 24 | 25 | export default MetadataLabels; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "target": "es2020", 7 | "jsx": "react", 8 | "allowJs": true, 9 | "strict": false, 10 | "noUnusedLocals": true, 11 | "sourceMap": true, 12 | "paths": { 13 | "@utils/*": ["src/utils/*"], 14 | "@gitops-utils/*": ["src/gitops/utils/*"], 15 | "@gitops-shared/*": ["src/gitops/components/shared/*"], 16 | "@gitops-models/*": ["src/gitops/models/*"], 17 | "@gitops-services/*": ["src/gitops/services/*"], 18 | "@rollout-models/*": ["src/rollout/models/*"], 19 | "@rollout-services/*": ["src/rollout/services/*"], 20 | "@es-models/*": ["src/externalsecrets/models/*"], 21 | "@images/*": ["src/images/*"] 22 | } 23 | }, 24 | "include": ["src"] 25 | } 26 | -------------------------------------------------------------------------------- /i18n-scripts/lexers.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const jsonc = require('comment-json'); 3 | 4 | /** 5 | * Custom JSON parser for localizing keys matching format: /%.+%/ 6 | */ 7 | module.exports.CustomJSONLexer = class extends EventEmitter { 8 | extract(content, filename) { 9 | let keys = []; 10 | console.log(1) 11 | try { 12 | jsonc.parse( 13 | content, 14 | (key, value) => { 15 | if (typeof value === 'string') { 16 | const match = value.match(/^%(.+)%$/); 17 | if (match && match[1]) { 18 | keys.push({ key: match[1] }); 19 | } 20 | } 21 | return value; 22 | }, 23 | true, 24 | ); 25 | } catch (e) { 26 | console.error('Failed to parse as JSON.', filename, e); 27 | keys = []; 28 | } 29 | return keys; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/externalsecrets/components/ESStatus.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { 4 | GreenCheckCircleIcon, 5 | RedExclamationCircleIcon 6 | } from '@openshift-console/dynamic-plugin-sdk'; 7 | 8 | import { ExternalSecretKind } from '@es-models/ExternalSecrets'; 9 | import { ExternalSecretStatus, getStatus } from '../utils/es-utils'; 10 | 11 | interface ESStatusProps { 12 | externalSecret: ExternalSecretKind 13 | } 14 | 15 | const ESStatus: React.FC = ({ externalSecret:es }) => { 16 | 17 | let status: ExternalSecretStatus = getStatus(es); 18 | let targetIcon: React.ReactNode = status.ready ? : ; 19 | 20 | return ( 21 | ( 22 | 23 | {targetIcon} {status.reason} 24 | 25 | ) 26 | ); 27 | }; 28 | 29 | export default ESStatus; 30 | -------------------------------------------------------------------------------- /test/apps/long/manifests/long-job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | annotations: 5 | argocd.argoproj.io/hook: PostSync 6 | argocd.argoproj.io/hook-delete-policy: BeforeHookCreation 7 | labels: 8 | test: reconcile 9 | name: long-job 10 | spec: 11 | suspend: true 12 | template: 13 | metadata: 14 | labels: 15 | job-name: long-job 16 | spec: 17 | containers: 18 | - command: 19 | - /bin/bash 20 | - -c 21 | - | 22 | sleep $DELAY 23 | env: 24 | - name: DELAY 25 | value: "12" 26 | image: registry.redhat.io/openshift-gitops-1/argocd-rhel8:1.7 27 | imagePullPolicy: IfNotPresent 28 | name: long-job 29 | restartPolicy: OnFailure 30 | schedulerName: default-scheduler 31 | serviceAccount: default 32 | serviceAccountName: default 33 | -------------------------------------------------------------------------------- /src/gitops/components/appset/generators/MergeGenerator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import GeneratorView from './GeneratorView'; 3 | import {CompressIcon} from '@patternfly/react-icons' 4 | import { MergeAppSetGenerator } from '@gitops-models/ApplicationSetModel'; 5 | import Generators from '../Generators'; 6 | 7 | interface MergeGeneratorProps { 8 | generator: MergeAppSetGenerator 9 | } 10 | 11 | export const MergeGeneratorFragment: React.FC = ({ generator }) => { 12 | return ( 13 | <> 14 | } title="Merge"> 15 | {generator.mergeKeys.length} merge keys 16 | 17 |
18 |
19 | 20 |
21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/gitops/components/appset/generators/GeneratorView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ReactNode } from 'react'; 3 | import { Card, CardBody, CardTitle, Divider, Icon } from '@patternfly/react-core'; 4 | 5 | 6 | type GeneratorViewProps = { 7 | title: string, 8 | icon?: JSX.Element, 9 | children?: ReactNode 10 | }; 11 | 12 | const GeneratorView = ({ title, icon, children }: GeneratorViewProps) => ( 13 | 14 | 15 |
{icon}{title}
16 | {children && 17 | 18 | } 19 |
20 | {children && 21 | {children} 22 | } 23 |
24 | ); 25 | export default GeneratorView; 26 | -------------------------------------------------------------------------------- /src/utils/components/EventsTab/EventsTab.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { RouteComponentProps } from 'react-router'; 3 | 4 | import { K8sResourceCommon, ResourceEventStream } from '@openshift-console/dynamic-plugin-sdk'; 5 | import { PageSection, Title } from '@patternfly/react-core'; 6 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 7 | 8 | 9 | type EventsTabProps = RouteComponentProps<{ 10 | ns: string; 11 | name: string; 12 | }> & { 13 | obj?: K8sResourceCommon; 14 | }; 15 | 16 | const EventsTab: React.FC = ({ obj }) => { 17 | const { t } = useGitOpsTranslation(); 18 | return !obj ? ( 19 |
20 | 21 | 22 | {t('Rollout details')} 23 | 24 | 25 |
26 | ) : ( 27 | 28 | ); 29 | }; 30 | 31 | export default EventsTab; 32 | -------------------------------------------------------------------------------- /src/gitops/components/appset/Status.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { ApplicationSetStatus } from '@gitops-utils/constants'; 4 | import { HealthDegradedIcon, HealthHealthyIcon, HealthUnknownIcon } from '@utils/components/Icons/Icons'; 5 | 6 | interface SyncProps { 7 | status: ApplicationSetStatus 8 | } 9 | 10 | const SyncStatus: React.FC = ({ status }) => { 11 | let targetIcon: React.ReactNode; 12 | switch (status) { 13 | case ApplicationSetStatus.HEALTHY: 14 | targetIcon = ; 15 | break; 16 | case ApplicationSetStatus.ERROR: 17 | targetIcon = ; 18 | break; 19 | case ApplicationSetStatus.UNKNOWN: 20 | targetIcon = ; 21 | break; 22 | } 23 | return ( 24 | ( 25 | 26 | {(status?targetIcon:"")} {status} 27 | 28 | ) 29 | ); 30 | }; 31 | 32 | export default SyncStatus; 33 | -------------------------------------------------------------------------------- /i18next-parser.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires, no-undef 2 | const { CustomJSONLexer } = require('./i18n-scripts/lexers'); 3 | 4 | // eslint-disable-next-line no-undef 5 | module.exports = { 6 | sort: true, 7 | createOldCatalogs: false, 8 | keySeparator: false, 9 | locales: ['en'], 10 | namespaceSeparator: '~', 11 | reactNamespace: false, 12 | defaultNamespace: 'plugin__console-plugin-template', 13 | useKeysAsDefaultValue: true, 14 | 15 | // see below for more details 16 | lexers: { 17 | hbs: ['HandlebarsLexer'], 18 | handlebars: ['HandlebarsLexer'], 19 | 20 | htm: ['HTMLLexer'], 21 | html: ['HTMLLexer'], 22 | 23 | mjs: ['JavascriptLexer'], 24 | js: ['JavascriptLexer'], // if you're writing jsx inside .js files, change this to JsxLexer 25 | ts: ['JavascriptLexer'], 26 | jsx: ['JsxLexer'], 27 | tsx: ['JsxLexer'], 28 | json: [CustomJSONLexer], 29 | 30 | default: ['JavascriptLexer'], 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /i18n-scripts/common.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | isDirectory(filePath) { 6 | try { 7 | const stat = fs.lstatSync(filePath); 8 | return stat.isDirectory(); 9 | } catch (e) { 10 | // lstatSync throws an error if path doesn't exist 11 | return false; 12 | } 13 | }, 14 | parseFolder(directory, argFunction, packageDir) { 15 | (async () => { 16 | try { 17 | const files = await fs.promises.readdir(directory); 18 | for (const file of files) { 19 | const filePath = path.join(directory, file); 20 | argFunction(filePath, packageDir); 21 | } 22 | } catch (e) { 23 | console.error(`Failed to parseFolder ${directory}:`, e); 24 | } 25 | })(); 26 | }, 27 | deleteFile(filePath) { 28 | try { 29 | fs.unlinkSync(filePath); 30 | } catch (e) { 31 | console.error(`Failed to delete file ${filePath}:`, e); 32 | } 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /src/gitops/components/application/ApplicationHistoryTab.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { RouteComponentProps } from 'react-router'; 3 | 4 | import HistoryList from './History/History'; 5 | import { ApplicationHistory, ApplicationKind } from '@gitops-models/ApplicationModel'; 6 | import { PageSection, PageSectionVariants } from '@patternfly/react-core'; 7 | 8 | type ApplicationHistoryTabProps = RouteComponentProps<{ 9 | ns: string; 10 | name: string; 11 | }> & { 12 | obj?: ApplicationKind; 13 | }; 14 | 15 | const ApplicationHistoryTab: React.FC = ({ obj }) => { 16 | 17 | var history: ApplicationHistory[]; 18 | if (obj?.status?.history) { 19 | history = obj?.status?.history; 20 | } else { 21 | history = []; 22 | } 23 | 24 | return ( 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default ApplicationHistoryTab; 32 | -------------------------------------------------------------------------------- /src/rollout/components/RolloutPodsTab.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { RouteComponentProps } from 'react-router'; 3 | 4 | import { PageSection, Title } from '@patternfly/react-core'; 5 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 6 | import { RolloutKind } from '../models/RolloutModel'; 7 | import { PodList } from '@utils/components/PodList/PodList'; 8 | 9 | type RolloutPodsTabProps = RouteComponentProps<{ 10 | ns: string; 11 | name: string; 12 | }> & { 13 | obj?: RolloutKind; 14 | }; 15 | 16 | const RolloutPodsTab: React.FC = ({ obj: rollout }) => { 17 | const { t } = useGitOpsTranslation(); 18 | return !rollout ? ( 19 |
20 | 21 | 22 | {t('Rollout details')} 23 | 24 | 25 |
26 | ) : ( 27 | 28 | ); 29 | }; 30 | 31 | export default RolloutPodsTab; 32 | -------------------------------------------------------------------------------- /src/gitops/components/appset/GeneratorsTab.tsx: -------------------------------------------------------------------------------- 1 | //import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; 2 | import * as React from 'react'; 3 | import { RouteComponentProps } from 'react-router'; 4 | import Generators from './Generators'; 5 | import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; 6 | import { ApplicationSetKind } from '@gitops-models/ApplicationSetModel'; 7 | import { PageSection } from '@patternfly/react-core'; 8 | 9 | type AppSetGeneratorsProps = RouteComponentProps<{ 10 | ns: string; 11 | name: string; 12 | }> & { 13 | obj?: K8sResourceCommon; 14 | }; 15 | 16 | const AppSetGeneratorsTab: React.FC = ({ obj }) => { 17 | console.log(obj) 18 | var appset:ApplicationSetKind = obj as ApplicationSetKind; 19 | 20 | return ( 21 | 22 | {appset?.spec?.generators && 23 | 24 | } 25 | 26 | ) 27 | } 28 | 29 | export default AppSetGeneratorsTab; 30 | -------------------------------------------------------------------------------- /src/gitops/components/application/Revision/Revision.tsx: -------------------------------------------------------------------------------- 1 | import { createRevisionURL } from 'src/gitops/utils/gitops'; 2 | import * as React from 'react'; 3 | import ExternalLink from '@gitops-shared/ExternalLink'; 4 | 5 | interface RevisionProps { 6 | repoURL: string; 7 | revision: string; 8 | helm: boolean; 9 | } 10 | 11 | const Revision: React.FC = ({ repoURL, revision, helm }) => { 12 | if (revision) { 13 | return ( 14 | ( 15 | <> 16 | {!helm && 17 | 18 | {revision.substring(0, 7) || ''} 19 | 20 | } 21 | {helm && 22 | { revision } 23 | } 24 | 25 | ) 26 | ) 27 | } else { 28 | return ( 29 | ( 30 | None 31 | ) 32 | ) 33 | } 34 | }; 35 | 36 | export default Revision; 37 | -------------------------------------------------------------------------------- /src/gitops/components/application/Statuses/SyncStatus.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { 4 | GreenCheckCircleIcon, 5 | } from '@openshift-console/dynamic-plugin-sdk'; 6 | 7 | import { SyncUnknownIcon, OutOfSyncIcon, SyncFailedIcon } from '@utils/components/Icons/Icons'; 8 | import { SyncStatus as SS } from '@gitops-utils/constants'; 9 | 10 | interface SyncProps { 11 | status: string; 12 | } 13 | 14 | const SyncStatus: React.FC = ({ status }) => { 15 | let targetIcon: React.ReactNode; 16 | if (status === SS.SYNCED) { 17 | targetIcon = ; 18 | } else if (status === SS.SYNCED_FAILED) { 19 | targetIcon = 20 | } else if (status === SS.OUT_OF_SYNC) { 21 | targetIcon = ; 22 | } else if (status === SS.PRUNE_SKIPPED) { 23 | targetIcon = 24 | } else { 25 | targetIcon = ; 26 | } 27 | return ( 28 | ( 29 | 30 | {(status?targetIcon:"")} {status} 31 | 32 | ) 33 | ); 34 | }; 35 | 36 | export default SyncStatus; 37 | -------------------------------------------------------------------------------- /test-prow-e2e.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -exuo pipefail 4 | 5 | ARTIFACT_DIR=${ARTIFACT_DIR:=/tmp/artifacts} 6 | SCREENSHOTS_DIR=integration-tests/screenshots 7 | INSTALLER_DIR=${INSTALLER_DIR:=${ARTIFACT_DIR}/installer} 8 | 9 | function copyArtifacts { 10 | if [ -d "$ARTIFACT_DIR" ] && [ -d "$SCREENSHOTS_DIR" ]; then 11 | if [[ -z "$(ls -A -- "$SCREENSHOTS_DIR")" ]]; then 12 | echo "No artifacts were copied." 13 | else 14 | echo "Copying artifacts from $(pwd)..." 15 | cp -r "$SCREENSHOTS_DIR" "${ARTIFACT_DIR}/screenshots" 16 | fi 17 | fi 18 | } 19 | 20 | trap copyArtifacts EXIT 21 | 22 | 23 | # don't log kubeadmin-password 24 | set +x 25 | BRIDGE_KUBEADMIN_PASSWORD="$(cat "${KUBEADMIN_PASSWORD_FILE:-${INSTALLER_DIR}/auth/kubeadmin-password}")" 26 | export BRIDGE_KUBEADMIN_PASSWORD 27 | set -x 28 | BRIDGE_BASE_ADDRESS="$(oc get consoles.config.openshift.io cluster -o jsonpath='{.status.consoleURL}')" 29 | export BRIDGE_BASE_ADDRESS 30 | 31 | echo "Install dependencies" 32 | if [ ! -d node_modules ]; then 33 | yarn install 34 | fi 35 | 36 | echo "Runs Cypress tests in headless mode" 37 | yarn run test-cypress-headless -------------------------------------------------------------------------------- /src/utils/components/DetailsDescriptionGroup/DetailsDescriptionGroup.tsx: -------------------------------------------------------------------------------- 1 | import { DescriptionListDescription, DescriptionListGroup, DescriptionListTermHelpText, DescriptionListTermHelpTextButton, Popover } from '@patternfly/react-core'; 2 | import * as React from 'react'; 3 | 4 | type DetailsDescriptionGroupProps = { 5 | title: string; 6 | help: string; 7 | } 8 | 9 | export const DetailsDescriptionGroup = (props: React.PropsWithChildren) => { 10 | 11 | return ( 12 | 13 | 14 | {props.title}} bodyContent={
{props.help}
}> 15 | {props.title} 16 |
17 |
18 | 19 | {props.children} 20 | 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/gitops/components/appset/generators/ValuesView.tsx: -------------------------------------------------------------------------------- 1 | import { ExpandableSection } from '@patternfly/react-core'; 2 | import { Table, Tbody, Td, Tr } from '@patternfly/react-table'; 3 | import * as React from 'react'; 4 | 5 | interface ValuesProps { 6 | values: Map 7 | } 8 | 9 | export const ValuesFragment: React.FC = ({ values }) => { 10 | 11 | const [isExpanded, setIsExpanded] = React.useState(false); 12 | 13 | const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => { 14 | setIsExpanded(isExpanded); 15 | }; 16 | 17 | return ( 18 | 19 | 20 | 21 | { 22 | Object.keys(values).map( (key) => { 23 | return ( 24 | 25 | ) 26 | }) 27 | } 28 | 29 |
{key}{values[key]}
30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/gitops/components/dashboards/ApplicationSets.tsx: -------------------------------------------------------------------------------- 1 | import { StatusGroupMapper } from '@openshift-console/dynamic-plugin-sdk'; 2 | 3 | import { ApplicationSetStatus } from '@gitops-utils/constants'; 4 | import { ApplicationSetKind } from '@gitops-models/ApplicationSetModel'; 5 | import { InventoryStatusGroup } from './dashboardUtils'; 6 | import { getAppSetStatus } from '@gitops-utils/gitops'; 7 | 8 | export const getApplicationSetStatusGroups: StatusGroupMapper = (appsets) => { 9 | const groups = { 10 | [InventoryStatusGroup.ERROR]: { 11 | count: 0, 12 | filterType: 'app-health', 13 | statusIDs: [ApplicationSetStatus.ERROR], 14 | }, 15 | [InventoryStatusGroup.NOT_MAPPED]: { 16 | count: 0, 17 | filterType: 'app-health', 18 | statusIDs: [ApplicationSetStatus.HEALTHY], 19 | }, 20 | [InventoryStatusGroup.UNKNOWN]: { 21 | count: 0, 22 | filterType: 'app-health', 23 | statusIDs: [ 24 | ApplicationSetStatus.UNKNOWN 25 | ], 26 | } 27 | }; 28 | 29 | appsets.forEach((appset: ApplicationSetKind) => { 30 | const group = 31 | Object.keys(groups).find((key) => 32 | groups[key].statusIDs.includes(getAppSetStatus(appset)) , 33 | ) || InventoryStatusGroup.NOT_MAPPED; 34 | groups[group].count++; 35 | }); 36 | 37 | return groups; 38 | }; 39 | -------------------------------------------------------------------------------- /locales/en/plugin__console-plugin-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "After cloning this project, replace references to": "After cloning this project, replace references to", 3 | "and other plugin metadata in package.json with values for your plugin.": "and other plugin metadata in package.json with values for your plugin.", 4 | "console-template-plugin": "console-template-plugin", 5 | "exposedModules": "exposedModules", 6 | "Hello, Plugin!": "Hello, Plugin!", 7 | "in package.json mapping the reference to the module.": "in package.json mapping the reference to the module.", 8 | "Plugin Example": "Plugin Example", 9 | "Success!": "Success!", 10 | "This is a custom page contributed by the console plugin template. The extension that adds the page is declared in console-extensions.json in the project root along with the corresponding nav item. Update console-extensions.json to change or add extensions. Code references in console-extensions.json must have a corresponding property": "This is a custom page contributed by the console plugin template. The extension that adds the page is declared in console-extensions.json in the project root along with the corresponding nav item. Update console-extensions.json to change or add extensions. Code references in console-extensions.json must have a corresponding property", 11 | "Your plugin is working.": "Your plugin is working." 12 | } -------------------------------------------------------------------------------- /src/gitops/components/appset/generators/GitGenerator.tsx: -------------------------------------------------------------------------------- 1 | import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from '@patternfly/react-core'; 2 | import * as React from 'react'; 3 | import { GitAppSetGenerator } from '@gitops-models/ApplicationSetModel'; 4 | import GeneratorView from './GeneratorView'; 5 | import {GitAltIcon} from '@patternfly/react-icons' 6 | 7 | interface GitGeneratorProps { 8 | generator: GitAppSetGenerator 9 | } 10 | 11 | export const GitGeneratorFragment: React.FC = ({ generator }) => { 12 | return ( 13 | } title={"git (" + (generator.files?"File":"Directory") + ")"}> 14 | 15 | 16 | Repository 17 | {generator.repoURL} 18 | 19 | 20 | Revision 21 | {generator.revision} 22 | 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/externalsecrets/models/ExternalSecrets.ts: -------------------------------------------------------------------------------- 1 | import { modelToRef } from "@gitops-utils/utils"; 2 | import { K8sModel, K8sResourceCommon, K8sResourceCondition } from "@openshift-console/dynamic-plugin-sdk"; 3 | 4 | export const ExternalSecretModel: K8sModel = { 5 | label: 'ExternalSecret', 6 | labelPlural: 'ExternalSecrets', 7 | apiVersion: 'v1beta1', 8 | apiGroup: 'external-secrets.io', 9 | plural: 'externalsecrets', 10 | abbr: 'es', 11 | namespaced: true, 12 | kind: 'ExternalSecret', 13 | id: 'externalsecret', 14 | crd: true 15 | }; 16 | 17 | export type ExternalSecretSpec = { 18 | refreshInterval?: string 19 | secretStoreRef?: { 20 | kind?: string 21 | name: string 22 | } 23 | target?: { 24 | creationPolicy?: string 25 | deletionPolicy?: string 26 | immutable?: boolean 27 | name?: string 28 | template?: Object 29 | } 30 | } 31 | 32 | export type ExternalSecretStatus = { 33 | binding? : { 34 | name: string 35 | } 36 | conditions?: K8sResourceCondition[] 37 | refreshTime?: string 38 | synchedResourceVersion?: string 39 | } 40 | 41 | export type ExternalSecretKind = K8sResourceCommon & { 42 | spec?: ExternalSecretSpec 43 | status?: ExternalSecretStatus 44 | }; 45 | 46 | export const externalSecretModelRef = modelToRef(ExternalSecretModel); 47 | -------------------------------------------------------------------------------- /test/appset/matrix/list-and-git.yaml: -------------------------------------------------------------------------------- 1 | # This example demonstrates the combining of the git generator with a list generator 2 | # The expected output would be an application per git directory and a list entry (application_count = git directory * list entries) 3 | # 4 | # 5 | apiVersion: argoproj.io/v1alpha1 6 | kind: ApplicationSet 7 | metadata: 8 | name: list-git 9 | spec: 10 | generators: 11 | - matrix: 12 | generators: 13 | - git: 14 | repoURL: https://github.com/argoproj/applicationset.git 15 | revision: HEAD 16 | directories: 17 | - path: examples/matrix/cluster-addons/* 18 | - list: 19 | elements: 20 | - cluster: engineering-dev 21 | url: https://1.2.3.4 22 | values: 23 | project: cluster-config 24 | - cluster: engineering-prod 25 | url: https://2.4.6.8 26 | values: 27 | project: cluster-config 28 | template: 29 | metadata: 30 | # annotations: 31 | # argocd.argoproj.io/skip-reconcile: "true" 32 | labels: 33 | appset-test: "true" 34 | name: '{{path.basename}}-{{cluster}}' 35 | spec: 36 | project: '{{values.project}}' 37 | source: 38 | repoURL: https://github.com/argoproj/applicationset.git 39 | targetRevision: HEAD 40 | path: '{{path}}' 41 | destination: 42 | server: '{{url}}' 43 | namespace: '{{path.basename}}' 44 | -------------------------------------------------------------------------------- /src/rollout/components/RolloutRevisionsTab.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { RouteComponentProps } from 'react-router'; 3 | 4 | import { PageSection, Title } from '@patternfly/react-core'; 5 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 6 | import { RolloutKind } from '../models/RolloutModel'; 7 | import { Revisions } from './Revisions/Revisions'; 8 | import { resourceAsArray } from '@gitops-utils/utils'; 9 | import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; 10 | 11 | type RolloutRevisionsTabProps = RouteComponentProps<{ 12 | ns: string; 13 | name: string; 14 | }> & { 15 | obj?: RolloutKind; 16 | }; 17 | 18 | const RolloutRevisionsTab: React.FC = ({ obj: rollout }) => { 19 | const { t } = useGitOpsTranslation(); 20 | 21 | const [replicaSets] = useK8sWatchResource({ 22 | groupVersionKind: { group: 'apps', version: 'v1', kind: 'ReplicaSet' }, 23 | isList: true, 24 | namespaced: true, 25 | namespace: rollout.metadata?.namespace, 26 | selector: rollout.spec.selector 27 | }); 28 | 29 | return !rollout ? ( 30 |
31 | 32 | 33 | {t('Rollout details')} 34 | 35 | 36 |
37 | ) : ( 38 | 39 | ); 40 | }; 41 | 42 | export default RolloutRevisionsTab; 43 | -------------------------------------------------------------------------------- /src/externalsecrets/utils/es-utils.ts: -------------------------------------------------------------------------------- 1 | import { ExternalSecretKind } from "@es-models/ExternalSecrets"; 2 | 3 | export enum ConditionReason { 4 | SecretSynced = "SecretSynced", 5 | // ConditionReasonSecretSyncedError indicates that there was an error syncing the secret. 6 | SecretSyncedError = "SecretSyncedError", 7 | // ConditionReasonSecretDeleted indicates that the secret has been deleted. 8 | SecretDeleted = "SecretDeleted", 9 | InvalidStoreRef = "InvalidStoreRef", 10 | InvalidProviderClientConfig = "InvalidProviderClientConfig", 11 | UpdateFailed = "UpdateFailed", 12 | Updated = "Updated", 13 | Unknown = "Unknown" 14 | } 15 | 16 | export type ExternalSecretStatus = { 17 | reason: string 18 | ready: boolean 19 | } 20 | 21 | // TODO - This needs improvement, we iterate but is it necessary? 22 | export function getStatus(es: ExternalSecretKind): ExternalSecretStatus { 23 | let status: ExternalSecretStatus = {reason: "Unknown", ready: false} 24 | if (es.status?.conditions) { 25 | es.status.conditions.forEach((condition) => { 26 | if (condition.type == "Ready") { 27 | status = {reason: condition.reason ? condition.reason: "" , ready: condition.status == "True"} 28 | return; 29 | } 30 | }) 31 | } 32 | return status; 33 | } 34 | 35 | export function getTargetSecretName(es: ExternalSecretKind):string { 36 | return es.spec.target?.name ? es.spec.target.name : es.metadata.name; 37 | } 38 | -------------------------------------------------------------------------------- /src/gitops/components/appset/generators/ClusterGenerator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import GeneratorView from './GeneratorView'; 3 | import {ClusterIcon} from '@patternfly/react-icons' 4 | 5 | import { ClusterAppSetGenerator } from '@gitops-models/ApplicationSetModel'; 6 | import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from '@patternfly/react-core'; 7 | import { ValuesFragment } from './ValuesView'; 8 | 9 | interface ClusterGeneratorProps { 10 | generator: ClusterAppSetGenerator 11 | } 12 | 13 | export const ClusterGeneratorFragment: React.FC = ({ generator }) => { 14 | 15 | console.log("Selector"); 16 | console.log(generator.selector); 17 | 18 | return ( 19 | } title="Cluster"> 20 | {generator.selector && 21 | <> 22 | 23 | 24 | Selector 25 | 26 | {JSON.stringify(generator.selector)} 27 | 28 | 29 | 30 |
31 | 32 | } 33 | {generator.values && 34 | 35 | } 36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /manifests/base/proxy-deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: gitops-plugin-proxy 6 | app.kubernetes.io/instance: gitops-plugin-proxy 7 | app.openshift.io/runtime: go 8 | name: gitops-plugin-proxy 9 | spec: 10 | progressDeadlineSeconds: 600 11 | replicas: 1 12 | revisionHistoryLimit: 10 13 | selector: 14 | matchLabels: 15 | name: gitops-plugin-proxy 16 | strategy: 17 | rollingUpdate: 18 | maxSurge: 25% 19 | maxUnavailable: 25% 20 | type: RollingUpdate 21 | template: 22 | metadata: 23 | creationTimestamp: null 24 | labels: 25 | name: gitops-plugin-proxy 26 | spec: 27 | containers: 28 | - image: quay.io/gnunn/gitops-plugin-proxy:latest 29 | imagePullPolicy: Always 30 | name: gitops-plugin-proxy 31 | ports: 32 | - containerPort: 8443 33 | protocol: TCP 34 | resources: 35 | limits: 36 | memory: 512Mi 37 | requests: 38 | cpu: 100m 39 | memory: 250Mi 40 | terminationMessagePath: /dev/termination-log 41 | terminationMessagePolicy: File 42 | volumeMounts: 43 | - mountPath: /mnt/certs 44 | name: gitops-plugin-proxy-certs 45 | readOnly: true 46 | dnsPolicy: ClusterFirst 47 | restartPolicy: Always 48 | schedulerName: default-scheduler 49 | terminationGracePeriodSeconds: 30 50 | volumes: 51 | - name: gitops-plugin-proxy-certs 52 | secret: 53 | defaultMode: 420 54 | secretName: gitops-plugin-proxy-certs 55 | -------------------------------------------------------------------------------- /src/rollout/components/Strategy/CanaryServices.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { RolloutKind } from "@rollout-models/RolloutModel"; 4 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 5 | import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; 6 | import { DetailsDescriptionGroup } from '@utils/components/DetailsDescriptionGroup/DetailsDescriptionGroup'; 7 | 8 | type CanaryServicesProps = { 9 | rollout: RolloutKind; 10 | } 11 | 12 | const CanaryServices: React.FC = ({ rollout }) => { 13 | const { t } = useGitOpsTranslation(); 14 | 15 | return ( 16 | <> 17 | 18 | {rollout.spec.strategy.canary.stableService ? 19 | 20 | : 21 | "-" 22 | } 23 | 24 | 25 | 26 | {rollout.spec.strategy.canary.canaryService ? 27 | 28 | : 29 | "-" 30 | } 31 | 32 | 33 | ) 34 | } 35 | 36 | export default CanaryServices; 37 | -------------------------------------------------------------------------------- /src/rollout/components/Strategy/BlueGreenServices.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { RolloutKind } from "@rollout-models/RolloutModel"; 4 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 5 | import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; 6 | import { DetailsDescriptionGroup } from '@utils/components/DetailsDescriptionGroup/DetailsDescriptionGroup'; 7 | 8 | type BlueGreenServicesProps = { 9 | rollout: RolloutKind; 10 | } 11 | 12 | const BlueGreenServices: React.FC = ({ rollout }) => { 13 | const { t } = useGitOpsTranslation(); 14 | 15 | return ( 16 | <> 17 | 18 | {rollout.spec.strategy.blueGreen.activeService ? 19 | 20 | : 21 | "-" 22 | } 23 | 24 | 25 | 26 | {rollout.spec.strategy.blueGreen.previewService ? 27 | 28 | : 29 | "-" 30 | } 31 | 32 | 33 | ) 34 | } 35 | 36 | export default BlueGreenServices; 37 | -------------------------------------------------------------------------------- /integration-tests/support/login.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace Cypress { 3 | interface Chainable { 4 | login(username?: string, password?: string): Chainable; 5 | logout(): Chainable; 6 | } 7 | } 8 | } 9 | 10 | const KUBEADMIN_USERNAME = 'kubeadmin'; 11 | const loginUsername = Cypress.env('BRIDGE_KUBEADMIN_PASSWORD') 12 | ? 'user-dropdown' 13 | : 'username'; 14 | 15 | // This will add 'cy.login(...)' 16 | // ex: cy.login('my-user', 'my-password') 17 | Cypress.Commands.add('login', (username: string, password: string) => { 18 | // Check if auth is disabled (for a local development environment). 19 | cy.visit('/'); // visits baseUrl which is set in plugins/index.js 20 | cy.window().then((win) => { 21 | if (win.SERVER_FLAGS?.authDisabled) { 22 | return; 23 | } 24 | 25 | // Make sure we clear the cookie in case a previous test failed to logout. 26 | cy.clearCookie('openshift-session-token'); 27 | 28 | cy.get('#inputUsername').type(username || KUBEADMIN_USERNAME); 29 | cy.get('#inputPassword').type( 30 | password || Cypress.env('BRIDGE_KUBEADMIN_PASSWORD'), 31 | ); 32 | cy.get('button[type=submit]').click(); 33 | 34 | cy.get(`[data-test="${loginUsername}"]`).should('be.visible'); 35 | }); 36 | }); 37 | 38 | Cypress.Commands.add('logout', () => { 39 | // Check if auth is disabled (for a local development environment). 40 | cy.window().then((win) => { 41 | if (win.SERVER_FLAGS?.authDisabled) { 42 | return; 43 | } 44 | cy.get('[data-test="user-dropdown"]').click(); 45 | cy.get('[data-test="log-out"]').should('be.visible'); 46 | cy.get('[data-test="log-out"]').click({ force: true }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/rollout/components/Revisions/RevisionsRowActions.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 3 | import { ReplicaSetInfo, ReplicaSetStatus } from 'src/rollout/utils/ReplicaSetInfo'; 4 | import { rollbackRollout } from '@rollout-services/Rollout'; 5 | import { RolloutKind } from '@rollout-models/RolloutModel'; 6 | import { Dropdown, DropdownItem, KebabToggle } from '@patternfly/react-core/deprecated'; 7 | 8 | type RevisionsRowActionsProps = { 9 | rollout: RolloutKind, 10 | rsInfo: ReplicaSetInfo; 11 | }; 12 | 13 | export const RevisionsRowActions: React.FC = ({ rollout, rsInfo }) => { 14 | 15 | const [isOpen, setIsOpen] = React.useState(false); 16 | 17 | const { t } = useGitOpsTranslation(); 18 | 19 | const onRollback = () => { 20 | rollbackRollout(rollout, rsInfo.replicaSet); 21 | }; 22 | 23 | const onToggle = (_event: any, isOpen: boolean) => { 24 | setIsOpen(isOpen); 25 | }; 26 | 27 | return ( 28 | setIsOpen(false)} 31 | toggle={} 32 | isOpen={isOpen} 33 | isPlain 34 | dropdownItems={[ 35 | 36 | {t('Rollback')} 37 | 38 | ]} 39 | /> 40 | ); 41 | 42 | } 43 | 44 | export const getContentScrollableElement = (): HTMLElement => 45 | document.getElementById('content-scrollable'); 46 | -------------------------------------------------------------------------------- /src/rollout/components/RolloutStatus.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { RolloutStatusDegradedIcon, RolloutStatusHealthyIcon, RolloutStatusPausedIcon, RolloutStatusProgressingIcon, RolloutStatusUnknownIcon } from '@utils/components/Icons/Icons'; 3 | import { RolloutStatus } from 'src/rollout/utils/rollout-utils'; 4 | import { InfoCircleIcon} from '@patternfly/react-icons'; 5 | import { Tooltip } from '@patternfly/react-core'; 6 | 7 | interface RolloutStatusProps { 8 | status: RolloutStatus; 9 | message?: string 10 | } 11 | 12 | export const RolloutStatusFragment: React.FC = ({ status, message }) => { 13 | 14 | let icon: React.ReactNode; 15 | switch (status) { 16 | case RolloutStatus.Progressing: { 17 | icon = 18 | break; 19 | } 20 | case RolloutStatus.Degraded: { 21 | icon = ; 22 | break; 23 | } 24 | case RolloutStatus.Paused: { 25 | icon = ; 26 | break; 27 | } 28 | case RolloutStatus.Healthy: { 29 | icon = ; 30 | break 31 | } 32 | default: 33 | icon = ; 34 | } 35 | return ( 36 | 37 | {icon} {status} {status == RolloutStatus.Degraded && message && 38 | 41 | 42 | 43 | } 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/gitops/components/project/ResourceAllowDenyList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { RowProps, TableColumn, TableData, VirtualizedTable } from '@openshift-console/dynamic-plugin-sdk'; 4 | import { ResourceAllowDeny } from '@gitops-models/AppProjectModel'; 5 | 6 | interface ResourceAllowDenyListProps { 7 | list: ResourceAllowDeny[] 8 | } 9 | 10 | const ResourceAllowDenyList: React.FC = ({ list }) => { 11 | return ( 12 | <> 13 | 21 | 22 | ) 23 | } 24 | 25 | const resourceAllowDenyListRow: React.FC> = ({ obj, activeColumnIDs }) => { 26 | 27 | return ( 28 | <> 29 | 30 | {obj.kind} 31 | 32 | 33 | {obj.group?obj.group:"-"} 34 | 35 | 36 | ); 37 | }; 38 | 39 | export const useResourceAllowDenyColumns = () => { 40 | 41 | const columns: TableColumn[] = React.useMemo( 42 | () => [ 43 | { 44 | title: 'Kind', 45 | id: 'kind' 46 | }, 47 | { 48 | title: 'Group', 49 | id: 'group' 50 | } 51 | ], 52 | [], 53 | ); 54 | 55 | return columns; 56 | }; 57 | 58 | export default ResourceAllowDenyList; 59 | -------------------------------------------------------------------------------- /src/gitops/models/AppProjectModel.ts: -------------------------------------------------------------------------------- 1 | import { modelToRef } from '@gitops-utils/utils'; 2 | import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; 3 | import { K8sModel } from '@openshift-console/dynamic-plugin-sdk/lib/api/common-types'; 4 | 5 | export const AppProjectModel: K8sModel = { 6 | label: 'AppProject', 7 | labelPlural: 'AppProjects', 8 | apiVersion: 'v1alpha1', 9 | apiGroup: 'argoproj.io', 10 | plural: 'appprojects', 11 | abbr: 'approj', 12 | namespaced: true, 13 | kind: 'AppProject', 14 | id: 'appproject', 15 | crd: true, 16 | }; 17 | 18 | export type ResourceAllowDeny = { 19 | group: string, 20 | kind: string 21 | } 22 | 23 | export type Destination = { 24 | name?: string, 25 | namespace: string, 26 | server: string 27 | } 28 | 29 | export type Role = { 30 | name: string, 31 | description?: string, 32 | groups?: string[], 33 | policies?: string[] 34 | } 35 | 36 | export type SyncWindow = { 37 | kind: string, 38 | schedule: string, 39 | duration: string, 40 | applications?: string[], 41 | clusters?: string[], 42 | namespaces?: string[], 43 | manualSync?: boolean, 44 | timeZone?: string 45 | } 46 | 47 | export type AppProjectKind = K8sResourceCommon & { 48 | spec?: { 49 | description?: string, 50 | destinations?: Destination[], 51 | sourceNamespaces?: string[], 52 | sourceRepos?: string[], 53 | clusterResourceWhitelist?: ResourceAllowDeny[], 54 | clusterResourceBlacklist?: ResourceAllowDeny[], 55 | namespaceResourceWhitelist?: ResourceAllowDeny[], 56 | namespaceResourceBlacklist?: ResourceAllowDeny[], 57 | roles?: Role[], 58 | syncWindows: SyncWindow[] 59 | }; 60 | status?: { [key: string]: any }; 61 | }; 62 | 63 | export const appProjectModelRef = modelToRef(AppProjectModel); 64 | -------------------------------------------------------------------------------- /src/gitops/components/application/Statuses/HealthStatus.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { HealthDegradedIcon, HealthHealthyIcon, HealthMissingIcon, HealthProgressingIcon, HealthSuspendedIcon, HealthUnknownIcon } from '@utils/components/Icons/Icons'; 4 | import { HealthStatus as HS } from 'src/gitops/utils/constants'; 5 | import { Button, Popover } from '@patternfly/react-core'; 6 | 7 | interface HealthProps { 8 | status: string; 9 | message?: string 10 | } 11 | 12 | const HealthStatus: React.FC = ({ status, message }) => { 13 | let targetIcon: React.ReactNode; 14 | switch (status) { 15 | case HS.HEALTHY: 16 | targetIcon = ; 17 | break; 18 | case HS.DEGRADED: 19 | targetIcon = ; 20 | break; 21 | case HS.SUSPENDED: 22 | targetIcon = ; 23 | break; 24 | case HS.MISSING: 25 | targetIcon = ; 26 | break; 27 | case HS.PROGRESSING: 28 | targetIcon = ; 29 | break; 30 | default: 31 | targetIcon = < HealthUnknownIcon />; 32 | } 33 | 34 | let showStatus: React.ReactFragment; 35 | if (message) { 36 | showStatus = ( 37 |
38 | {status}
} 41 | bodyContent={
{message}
} 42 | > 43 | 44 | 45 | 46 | ) 47 | } else { 48 | showStatus = ( 49 |
{targetIcon} {status}
50 | ) 51 | } 52 | 53 | 54 | return ( 55 | ( 56 |
57 | {showStatus} 58 |
59 | ) 60 | ); 61 | }; 62 | 63 | export default HealthStatus; 64 | -------------------------------------------------------------------------------- /src/utils/components/ActionDropDownItem/ActionDropDownItem.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Dispatch, FC, SetStateAction } from 'react'; 3 | import classNames from 'classnames'; 4 | 5 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 6 | import { Action, useAccessReview } from '@openshift-console/dynamic-plugin-sdk'; 7 | import { DropdownItem, TooltipPosition } from '@patternfly/react-core'; 8 | 9 | import './action-dropdown-item.scss'; 10 | 11 | type ActionDropdownItemProps = { 12 | action: Action; 13 | setIsOpen: Dispatch>; 14 | }; 15 | 16 | const ActionDropdownItem: FC = ({ action, setIsOpen }) => { 17 | const { t } = useGitOpsTranslation(); 18 | const [actionAllowed] = useAccessReview(action?.accessReview); 19 | const isCloneDisabled = !actionAllowed && action?.id === 'vm-action-clone'; 20 | 21 | const handleClick = () => { 22 | if (typeof action?.cta === 'function') { 23 | action?.cta(); 24 | setIsOpen(false); 25 | } 26 | }; 27 | 28 | return ( 29 | 43 | {action?.label} 44 | {action?.icon && ( 45 | <> 46 | {' '} 47 | {action.icon} 48 | 49 | )} 50 | 51 | ); 52 | }; 53 | 54 | export default ActionDropdownItem; 55 | -------------------------------------------------------------------------------- /src/rollout/models/AnalysisRunModel.ts: -------------------------------------------------------------------------------- 1 | import { modelToRef } from "@gitops-utils/utils"; 2 | import { K8sModel, K8sResourceCommon } from "@openshift-console/dynamic-plugin-sdk"; 3 | 4 | export const AnalysisRunModel: K8sModel = { 5 | label: 'AnalysisRun', 6 | labelPlural: 'AnalysisRuns', 7 | apiVersion: 'v1alpha1', 8 | apiGroup: 'argoproj.io', 9 | plural: 'analysisruns', 10 | abbr: 'ar', 11 | namespaced: true, 12 | kind: 'AnalysisRun', 13 | id: 'analysisrun', 14 | crd: true, 15 | }; 16 | 17 | export type Provider = { 18 | job?: any, 19 | prometheus?: any, 20 | datadog?: any, 21 | newRelic?: any, 22 | wavefront?: any, 23 | web?: any, 24 | kayenta?: any, 25 | cloudWatch?: any, 26 | graphite?: any, 27 | influxdb?: any, 28 | skywalking?: any 29 | } 30 | 31 | export type Metric = { 32 | count?: number, 33 | failureLimit?: number, 34 | interval?: string, 35 | name: string, 36 | provider: Provider, 37 | successCondition?: string 38 | } 39 | 40 | export type Measurement = { 41 | finishedAt?: string, 42 | metadata?: any, 43 | message?: string, 44 | phase: string, 45 | resumeAt: string, 46 | startedAt: string, 47 | value: string 48 | } 49 | 50 | export type MetricResult = { 51 | consecutiveError?: number, 52 | count?: number, 53 | dryRun?: boolean, 54 | error?: number, 55 | failed?: number, 56 | inconclusive?: number, 57 | measurements?: Measurement[], 58 | message?: string, 59 | name: string, 60 | phase: string, 61 | successful?: number 62 | } 63 | 64 | export type AnalysisRunSpec = { 65 | metrics?: Metric[] 66 | } 67 | 68 | export type AnaylsisRunStatus = { 69 | metricResults?: MetricResult[], 70 | phase?: string, 71 | startedAt?: string 72 | } 73 | 74 | export type AnalysisRunKind = K8sResourceCommon & { 75 | spec: AnalysisRunSpec, 76 | status?: AnaylsisRunStatus 77 | } 78 | 79 | export const analysisRunModelRef = modelToRef(AnalysisRunModel); 80 | -------------------------------------------------------------------------------- /src/utils/components/ActionDropDown/ActionDropDown.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { FC, memo, useState } from 'react'; 3 | 4 | import ActionDropdownItem from '@utils/components/ActionDropDownItem/ActionDropDownItem'; 5 | import DropdownToggle from '@utils/components/toggles/DropDownToggle'; 6 | import KebabToggle from '@utils/components/toggles/KebabToggle'; 7 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 8 | import { Action } from '@openshift-console/dynamic-plugin-sdk'; 9 | import { Dropdown, DropdownList } from '@patternfly/react-core'; 10 | 11 | type ActionsDropdownProps = { 12 | actions: Action[]; 13 | className?: string; 14 | id?: string; 15 | isKebabToggle?: boolean; 16 | onLazyClick?: () => void; 17 | }; 18 | 19 | const ActionsDropdown: FC = ({ 20 | actions = [], 21 | className, 22 | id, 23 | isKebabToggle, 24 | onLazyClick, 25 | }) => { 26 | const { t } = useGitOpsTranslation(); 27 | const [isOpen, setIsOpen] = useState(false); 28 | 29 | const onToggle = () => { 30 | setIsOpen((prevIsOpen) => { 31 | if (onLazyClick && !prevIsOpen) onLazyClick(); 32 | 33 | return !prevIsOpen; 34 | }); 35 | }; 36 | 37 | const Toggle = isKebabToggle 38 | ? KebabToggle({ isExpanded: isOpen, onClick: onToggle }) 39 | : DropdownToggle({ 40 | children: t('Actions'), 41 | isExpanded: isOpen, 42 | onClick: onToggle, 43 | }); 44 | 45 | return ( 46 | setIsOpen(open)} 51 | popperProps={{ enableFlip: true, position: 'right' }} 52 | toggle={Toggle} 53 | > 54 | 55 | {actions?.map((action) => ( 56 | 57 | ))} 58 | 59 | 60 | ); 61 | }; 62 | 63 | export default memo(ActionsDropdown); 64 | -------------------------------------------------------------------------------- /i18n-scripts/set-english-defaults.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const pluralize = require('pluralize'); 4 | const common = require('./common.js'); 5 | 6 | const publicDir = path.join(__dirname, './../locales/'); 7 | 8 | function determineRule(key) { 9 | if (key.includes('WithCount_plural')) { 10 | return 0; 11 | } 12 | if (key.includes('WithCount')) { 13 | return 1; 14 | } 15 | if (key.includes('_plural')) { 16 | return 2; 17 | } 18 | return 3; 19 | } 20 | 21 | function updateFile(fileName) { 22 | const file = require(fileName); 23 | const updatedFile = {}; 24 | 25 | const keys = Object.keys(file); 26 | 27 | let originalKey; 28 | 29 | for (let i = 0; i < keys.length; i++) { 30 | if (file[keys[i]] === '') { 31 | // follow i18next rules 32 | // "key": "item", 33 | // "key_plural": "items", 34 | // "keyWithCount": "{{count}} item", 35 | // "keyWithCount_plural": "{{count}} items" 36 | switch (determineRule(keys[i])) { 37 | case 0: 38 | [originalKey] = keys[i].split('WithCount_plural'); 39 | updatedFile[keys[i]] = `{{count}} ${pluralize(originalKey)}`; 40 | break; 41 | case 1: 42 | [originalKey] = keys[i].split('WithCount'); 43 | updatedFile[keys[i]] = `{{count}} ${originalKey}`; 44 | break; 45 | case 2: 46 | [originalKey] = keys[i].split('_plural'); 47 | updatedFile[keys[i]] = pluralize(originalKey); 48 | break; 49 | default: 50 | updatedFile[keys[i]] = keys[i]; 51 | } 52 | } else { 53 | updatedFile[keys[i]] = file[keys[i]]; 54 | } 55 | } 56 | 57 | fs.promises 58 | .writeFile(fileName, JSON.stringify(updatedFile, null, 2)) 59 | .catch((e) => console.error(fileName, e)); 60 | } 61 | 62 | function processLocalesFolder(filePath) { 63 | if (path.basename(filePath) === 'en') { 64 | common.parseFolder(filePath, updateFile); 65 | } 66 | } 67 | 68 | common.parseFolder(publicDir, processLocalesFolder); 69 | -------------------------------------------------------------------------------- /src/gitops/components/dashboards/Applications.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { StatusGroupMapper } from '@openshift-console/dynamic-plugin-sdk'; 4 | 5 | import { HealthStatus } from '@gitops-utils/constants'; 6 | import { ApplicationKind } from '@gitops-models/ApplicationModel'; 7 | import { InventoryStatusGroup } from './dashboardUtils'; 8 | import { OutlinedPauseCircleIcon } from '@patternfly/react-icons'; 9 | 10 | import { global_disabled_color_100 } from '@patternfly/react-tokens/dist/js/global_disabled_color_100'; 11 | 12 | export const getApplicationStatusGroups: StatusGroupMapper = (apps) => { 13 | const groups = { 14 | [InventoryStatusGroup.ERROR]: { 15 | count: 0, 16 | filterType: 'app-health', 17 | statusIDs: [HealthStatus.DEGRADED], 18 | }, 19 | [InventoryStatusGroup.NOT_MAPPED]: { 20 | count: 0, 21 | filterType: 'app-health', 22 | statusIDs: [HealthStatus.HEALTHY], 23 | }, 24 | [InventoryStatusGroup.PROGRESS]: { 25 | count: 0, 26 | filterType: 'app-health', 27 | statusIDs: [ 28 | HealthStatus.PROGRESSING 29 | ], 30 | }, 31 | [InventoryStatusGroup.UNKNOWN]: { 32 | count: 0, 33 | filterType: 'app-health', 34 | statusIDs: [ 35 | HealthStatus.UNKNOWN 36 | ], 37 | }, 38 | [InventoryStatusGroup.WARN]: { 39 | count: 0, 40 | filterType: 'app-health', 41 | statusIDs: [ 42 | HealthStatus.MISSING 43 | ], 44 | }, 45 | 'gitops-suspended': { 46 | count: 0, 47 | filterType: 'app-health', 48 | statusIDs: [ 49 | HealthStatus.SUSPENDED 50 | ], 51 | }, 52 | }; 53 | 54 | apps.forEach((app: ApplicationKind) => { 55 | const group = 56 | Object.keys(groups).find((key) => 57 | groups[key].statusIDs.includes(app.status?.health?.status) , 58 | ) || InventoryStatusGroup.NOT_MAPPED; 59 | groups[group].count++; 60 | }); 61 | 62 | return groups; 63 | }; 64 | 65 | export const HealthSuspendedIcon = 66 | -------------------------------------------------------------------------------- /src/gitops/components/project/ProjectDetailsTab.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { RouteComponentProps } from 'react-router'; 3 | import { 4 | DescriptionList, 5 | Grid, 6 | GridItem, 7 | PageSection, 8 | PageSectionVariants, 9 | Title 10 | } from '@patternfly/react-core'; 11 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 12 | import { AppProjectModel } from '@gitops-models/AppProjectModel'; 13 | import { getObjectModifyPermissions } from '@gitops-utils/utils'; 14 | import StandardDetailsGroup from '@utils/components/StandardDetailsGroup/StandardDetailsGroup'; 15 | import { DetailsDescriptionGroup } from '@utils/components/DetailsDescriptionGroup/DetailsDescriptionGroup'; 16 | 17 | type ProjectDetailsTabProps = RouteComponentProps<{ 18 | ns: string; 19 | name: string; 20 | }> & { 21 | obj?: any; 22 | }; 23 | 24 | const ProjectDetailsTab: React.FC = ({ obj }) => { 25 | const { t } = useGitOpsTranslation(); 26 | 27 | const [canPatch] = getObjectModifyPermissions(obj, AppProjectModel); 28 | 29 | return ( 30 |
31 | 32 | 33 | {t('Project details')} 34 | 35 | 36 | 37 | 38 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | {obj?.spec?.description} 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
59 | ); 60 | }; 61 | 62 | export default ProjectDetailsTab; 63 | -------------------------------------------------------------------------------- /src/gitops/components/appset/generators/ListGenerator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import GeneratorView from './GeneratorView'; 3 | import {ListIcon} from '@patternfly/react-icons' 4 | import { ListAppSetGenerator } from '@gitops-models/ApplicationSetModel'; 5 | import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; 6 | import { ExpandableSection } from '@patternfly/react-core'; 7 | 8 | interface ListGeneratorProps { 9 | generator: ListAppSetGenerator 10 | } 11 | 12 | export const ListGeneratorFragment: React.FC = ({ generator }) => { 13 | 14 | const [isExpanded, setIsExpanded] = React.useState(false); 15 | 16 | const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => { 17 | setIsExpanded(isExpanded); 18 | }; 19 | 20 | const displayValue = (value: any) => { 21 | if (value === undefined) return "null" 22 | else if (typeof value === "object") return JSON.stringify(value) 23 | else return value; 24 | } 25 | 26 | return ( 27 | } title="List"> 28 | 29 | 30 | 31 | 32 | {Object.keys(generator.elements[0]).map((key) => { 33 | return () 34 | })} 35 | 36 | 37 | 38 | {generator.elements.map((item) => { 39 | return ( 40 | 41 | {Object.values(item).map((val) => { 42 | return () 43 | })} 44 | 45 | ) 46 | })} 47 | 48 |
{key}
{displayValue(val)}
49 |
50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/externalsecrets/components/ESNavPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { 4 | HorizontalNav, 5 | useK8sWatchResource, 6 | } from '@openshift-console/dynamic-plugin-sdk'; 7 | import { Bullseye, Spinner } from '@patternfly/react-core'; 8 | 9 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 10 | import PageTitle from '@utils/components/PageTitle/PageTitle'; 11 | import { useESActionsProvider } from './hooks/useESActionsProvider'; 12 | import ResourceYAMLTab from '@utils/components/ResourceYAMLTab/ResourceYAMLTab'; 13 | import ESDetailsTab from './ESDetailsTab'; 14 | import { ExternalSecretKind, ExternalSecretModel } from '@es-models/ExternalSecrets'; 15 | import EventsTab from '@utils/components/EventsTab/EventsTab'; 16 | 17 | type ESPageProps = { 18 | name: string; 19 | namespace: string; 20 | kind: string; 21 | }; 22 | 23 | const ESNavPage: React.FC = ({ name, namespace, kind }) => { 24 | const { t } = useGitOpsTranslation(); 25 | const [es, loaded] = useK8sWatchResource({ 26 | groupVersionKind: { 27 | group: 'external-secrets.io', 28 | kind: 'ExternalSecret', 29 | version: 'v1beta1', 30 | }, 31 | kind, 32 | name, 33 | namespace, 34 | }); 35 | 36 | const [actions ] = useESActionsProvider(es); 37 | 38 | const pages = React.useMemo( 39 | () => [ 40 | { 41 | href: '', 42 | name: t('Details'), 43 | component: ESDetailsTab, 44 | }, 45 | { 46 | href: 'yaml', 47 | name: t('YAML'), 48 | component: ResourceYAMLTab, 49 | }, 50 | { 51 | href: 'events', 52 | name: t('Events'), 53 | component: EventsTab, 54 | } 55 | ], 56 | [], 57 | ); 58 | 59 | return ( 60 | <> 61 | 62 | {loaded ? ( 63 | 64 | ) : ( 65 | 66 | 67 | 68 | )} 69 | 70 | ); 71 | }; 72 | 73 | export default ESNavPage; 74 | -------------------------------------------------------------------------------- /plugin-metadata.ts: -------------------------------------------------------------------------------- 1 | import { ConsolePluginBuildMetadata } from '@openshift-console/dynamic-plugin-sdk-webpack'; 2 | 3 | const metadata: ConsolePluginBuildMetadata = { 4 | dependencies: { 5 | '@console/pluginAPI': '*', 6 | }, 7 | name: "gitops-admin-plugin", 8 | displayName: 'OpenShift GitOps Plugin', 9 | version: "0.2.0", 10 | description: "Administrator Perspective Console Plugin for OpenShift GitOps", 11 | exposedModules: { 12 | ApplicationList: "./gitops/components/application/ApplicationListTab.tsx", 13 | ApplicationDetails: "./gitops/components/application/ApplicationNavPage.tsx", 14 | yamlApplicationTemplates: "src/gitops/components/application/templates/index.ts", 15 | useApplicationActionsProvider: "./gitops/components/application/hooks/useApplicationActionsProvider.tsx", 16 | 17 | ProjectList: "./gitops/components/project/ProjectListTab.tsx", 18 | AppProjectDetails: "./gitops/components/project/ProjectNavPage.tsx", 19 | yamlAppProjectTemplates: "./gitops/components/project/templates/index.ts", 20 | useProjectActionsProvider: "./gitops/components/project/hooks/useProjectActionsProvider.tsx", 21 | 22 | ApplicationSetList: "./gitops/components/appset/AppSetListTab.tsx", 23 | ApplicationSetDetails: "./gitops/components/appset/AppSetNavPage.tsx", 24 | 25 | RolloutList: "./rollout/components/RolloutListTab.tsx", 26 | RolloutDetails: "./rollout/components/RolloutNavPage.tsx", 27 | useRolloutActionsProvider: "./rollout/components/hooks/useRolloutActionsProvider.tsx", 28 | yamlRolloutTemplates: "src/rollout/templates/index.ts", 29 | 30 | modalProvider: "./utils/components/ModalProvider/ModalProvider.tsx", 31 | 32 | dashboardUtils: "./gitops/components/dashboards/dashboardUtils.ts", 33 | ApplicationInventory: "./gitops/components/dashboards/Applications.tsx", 34 | ApplicationSetInventory: "./gitops/components/dashboards/ApplicationSets.tsx", 35 | 36 | ExternalSecretList: "./externalsecrets/components/ESListTab.tsx", 37 | ExternalSecretDetails: "./externalsecrets/components/ESNavPage.tsx", 38 | useESActionsProvider: "./externalsecrets/components/hooks/useESActionsProvider.tsx", 39 | } 40 | }; 41 | 42 | export default metadata; 43 | -------------------------------------------------------------------------------- /src/rollout/models/RolloutModel.ts: -------------------------------------------------------------------------------- 1 | import { modelToRef } from "@gitops-utils/utils"; 2 | import { K8sModel, K8sResourceCommon, Selector } from "@openshift-console/dynamic-plugin-sdk"; 3 | 4 | export const RolloutModel: K8sModel = { 5 | label: 'Rollout', 6 | labelPlural: 'Rollouts', 7 | apiVersion: 'v1alpha1', 8 | apiGroup: 'argoproj.io', 9 | plural: 'rollouts', 10 | abbr: 'ro', 11 | namespaced: true, 12 | kind: 'Rollout', 13 | id: 'rollout', 14 | crd: true, 15 | }; 16 | 17 | export type RolloutStrategyBlueGreen = { 18 | activeService: string, 19 | previewService: string, 20 | autoPromotionEnabled: boolean 21 | } 22 | 23 | export type RolloutStrategyCanary = { 24 | canaryService: string, 25 | stableService: string, 26 | autoPromotionEnabled: boolean 27 | } 28 | 29 | export type RolloutSpec = { 30 | replicas?: number, 31 | revisionHistoryLimit?: number, 32 | selector?: Selector, 33 | strategy: { 34 | blueGreen?: RolloutStrategyBlueGreen, 35 | canary?: RolloutStrategyCanary 36 | } 37 | } 38 | 39 | export type AnalysisRunStatus = { 40 | message?: string, 41 | name: string, 42 | status: string 43 | } 44 | 45 | export type PauseConditions = { 46 | reason: string, 47 | startTime: string 48 | } 49 | 50 | export type RolloutStatus = { 51 | blueGreen?: { 52 | activeSelector?: string, 53 | previewSelector?: string, 54 | postPromotionAnalysisRunStatus?: AnalysisRunStatus, 55 | prePromotionAnalysisRunStatus?: AnalysisRunStatus 56 | } 57 | controllerPause?: boolean, 58 | currentPodHash?: string, 59 | currentStepHash?: string, 60 | currentStepIndex?: number, 61 | message?: string, 62 | observedGeneration?: string, 63 | pauseConditions? : PauseConditions[], 64 | phase?: string, 65 | promoteFull: boolean, 66 | readyReplicas?: number, 67 | replicas?: number, 68 | selector?: string, 69 | stableRS?: string, 70 | updatedReplicas?: number 71 | } 72 | 73 | export type RolloutKind = K8sResourceCommon & { 74 | spec?: RolloutSpec, 75 | status?: RolloutStatus 76 | }; 77 | 78 | export const rolloutModelRef = modelToRef(RolloutModel); 79 | -------------------------------------------------------------------------------- /src/gitops/utils/urls.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Taken from the Argo CD UI from here: 3 | * https://github.com/argoproj/argo-cd/blob/4bd8b07c514e26c6b7837f30d52afd1a3cdedcfd/ui/src/app/shared/components/urls.ts 4 | */ 5 | 6 | import {GitUrl} from 'git-url-parse'; 7 | 8 | const GitUrlParse = require('git-url-parse'); 9 | 10 | export const isSHA = (revision: string) => { 11 | // https://stackoverflow.com/questions/468370/a-regex-to-match-a-sha1 12 | return revision.match(/^[a-f0-9]{5,40}$/) !== null; 13 | }; 14 | 15 | function supportedSource(parsed: GitUrl): boolean { 16 | return parsed.resource.startsWith('github') || ['gitlab.com', 'bitbucket.org'].indexOf(parsed.source) >= 0; 17 | } 18 | 19 | function protocol(proto: string): string { 20 | return proto === 'ssh' ? 'https' : proto; 21 | } 22 | 23 | export function repoUrl(url: string): string { 24 | try { 25 | const parsed = GitUrlParse(url); 26 | 27 | if (!supportedSource(parsed)) { 28 | return null; 29 | } 30 | 31 | return `${protocol(parsed.protocol)}://${parsed.resource}/${parsed.owner}/${parsed.name}`; 32 | } catch { 33 | return null; 34 | } 35 | } 36 | 37 | export function revisionUrl(url: string, revision: string, forPath: boolean): string { 38 | if (!revision) revision = "HEAD"; 39 | let parsed; 40 | try { 41 | parsed = GitUrlParse(url); 42 | } catch { 43 | return null; 44 | } 45 | let urlSubPath = isSHA(revision) ? 'commit' : 'tree'; 46 | 47 | if (url.indexOf('bitbucket') >= 0) { 48 | // The reason for the condition of 'forPath' is that when we build nested path, we need to use 'src' 49 | urlSubPath = isSHA(revision) && !forPath ? 'commits' : 'src'; 50 | } 51 | 52 | // Gitlab changed the way urls to commit look like 53 | // Ref: https://docs.gitlab.com/ee/update/deprecations.html#legacy-urls-replaced-or-removed 54 | if (parsed.source === 'gitlab.com') { 55 | urlSubPath = '-/' + urlSubPath; 56 | } 57 | 58 | if (!supportedSource(parsed)) { 59 | return null; 60 | } 61 | 62 | return `${protocol(parsed.protocol)}://${parsed.resource}/${parsed.owner}/${parsed.name}/${urlSubPath}/${revision || 'HEAD'}`; 63 | } -------------------------------------------------------------------------------- /manifests/overlays/install/plugin-patcher-job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | labels: 5 | app: gitops-admin-plugin 6 | app.kubernetes.io/instance: gitops-admin-plugin-job 7 | app.kubernetes.io/part-of: gitops-admin-plugin 8 | name: gitops-admin-plugin-patcher 9 | spec: 10 | template: 11 | metadata: 12 | creationTimestamp: null 13 | labels: 14 | app: gitops-admin-plugin 15 | app.kubernetes.io/instance: gitops-admin-plugin 16 | app.kubernetes.io/name: gitops-admin-plugin 17 | app.kubernetes.io/part-of: gitops-admin-plugin 18 | job-name: gitops-admin-plugin-patcher 19 | spec: 20 | containers: 21 | - command: 22 | - /bin/bash 23 | - -c 24 | - | 25 | existingPlugins=$(oc get consoles.operator.openshift.io cluster -o json | jq -c '.spec.plugins // []') 26 | mergedPlugins=$(jq --argjson existingPlugins "${existingPlugins}" --argjson consolePlugin '["gitops-admin-plugin"]' -c -n '$existingPlugins + $consolePlugin | unique') 27 | patchedPlugins=$(jq --argjson mergedPlugins $mergedPlugins -n -c '{ "spec": { "plugins": $mergedPlugins } }') 28 | oc patch consoles.operator.openshift.io cluster --patch $patchedPlugins --type=merge 29 | image: registry.redhat.io/openshift4/ose-tools-rhel8@sha256:e44074f21e0cca6464e50cb6ff934747e0bd11162ea01d522433a1a1ae116103 30 | imagePullPolicy: IfNotPresent 31 | name: gitops-admin-plugin-patcher 32 | resources: 33 | requests: 34 | cpu: 10m 35 | memory: 50Mi 36 | securityContext: 37 | allowPrivilegeEscalation: false 38 | capabilities: 39 | drop: 40 | - ALL 41 | terminationMessagePath: /dev/termination-log 42 | terminationMessagePolicy: File 43 | dnsPolicy: ClusterFirst 44 | restartPolicy: OnFailure 45 | schedulerName: default-scheduler 46 | securityContext: 47 | runAsNonRoot: true 48 | seccompProfile: 49 | type: RuntimeDefault 50 | serviceAccount: gitops-admin-plugin-patcher 51 | serviceAccountName: gitops-admin-plugin-patcher 52 | terminationGracePeriodSeconds: 400 -------------------------------------------------------------------------------- /src/utils/components/PageTitle/PageTitle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { DEFAULT_NAMESPACE } from '@gitops-utils/constants'; 4 | import { Breadcrumb, BreadcrumbItem, Spinner } from '@patternfly/react-core'; 5 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 6 | import { isApplicationRefreshing } from '@gitops-utils/gitops'; 7 | import { Action, K8sModel, K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; 8 | import ActionDropDown from '../ActionDropDown/ActionDropDown'; 9 | 10 | type ApplicationPageTitleProps = { 11 | obj: K8sResourceCommon; 12 | model: K8sModel; 13 | name: string; 14 | namespace: string; 15 | actions: Action[]; 16 | }; 17 | 18 | const ApplicationPageTitle: React.FC = ({ obj, model, name, namespace, actions }) => { 19 | const { t } = useGitOpsTranslation(); 20 | 21 | return ( 22 | <> 23 |
24 | 25 | 26 | 27 | {t(model.labelPlural)} 28 | 29 | 30 | {t(model.labelPlural + ' Details')} 31 | 32 |
33 |
34 | 35 |

36 | {'A'} 37 | 38 | {name ?? obj?.metadata?.name}{' '}{isApplicationRefreshing(obj) ? : } 39 | 40 |

41 |
42 | 43 |
44 |
45 |
46 | 47 | ); 48 | }; 49 | 50 | export default ApplicationPageTitle; 51 | -------------------------------------------------------------------------------- /src/rollout/services/Rollout.ts: -------------------------------------------------------------------------------- 1 | import { Patch, k8sPatch } from "@openshift-console/dynamic-plugin-sdk" 2 | import { RolloutKind, RolloutModel } from "@rollout-models/RolloutModel" 3 | 4 | export const retryRollout = async (rollout: RolloutKind): Promise => { 5 | return k8sPatch({ 6 | model: RolloutModel, 7 | resource: rollout, 8 | data: [{ 9 | op: 'add', 10 | path: '/status/abort', 11 | value: false 12 | }], 13 | path: "status" 14 | }) 15 | } 16 | 17 | export const abortRollout = async (rollout: RolloutKind): Promise => { 18 | return k8sPatch({ 19 | model: RolloutModel, 20 | resource: rollout, 21 | data: [{ 22 | op: 'add', 23 | path: '/status/abort', 24 | value: true 25 | }], 26 | path: "status" 27 | }) 28 | } 29 | 30 | export const restartRollout = async (rollout: RolloutKind): Promise => { 31 | const now = new Date().toISOString(); 32 | 33 | return k8sPatch({ 34 | model: RolloutModel, 35 | resource: rollout, 36 | data: [{ 37 | op: 'replace', 38 | path: '/spec/restartAt', 39 | value: now 40 | }] 41 | }) 42 | } 43 | 44 | export const rollbackRollout = async (rollout: RolloutKind, rs: any): Promise => { 45 | 46 | return k8sPatch({ 47 | model: RolloutModel, 48 | resource: rollout, 49 | data: [{ 50 | op: 'replace', 51 | path: '/spec/template', 52 | value: rs.spec.template 53 | }] 54 | }) 55 | } 56 | 57 | export const promoteRollout = async (rollout: RolloutKind, promoteFull: boolean): Promise => { 58 | 59 | const patch: Patch[] = []; 60 | if (promoteFull) { 61 | patch.push({ 62 | op: 'add', 63 | path: '/status/promoteFull', 64 | value: true 65 | }) 66 | } 67 | patch.push({ 68 | op: 'replace', 69 | path: '/status/pauseConditions', 70 | value: null 71 | }) 72 | 73 | return k8sPatch({ 74 | model: RolloutModel, 75 | resource: rollout, 76 | data: patch, 77 | path: "status" 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /manifests/base/plugin-deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: gitops-admin-plugin 6 | app.kubernetes.io/instance: gitops-admin-plugin 7 | app.openshift.io/runtime: nodejs 8 | name: gitops-admin-plugin 9 | spec: 10 | progressDeadlineSeconds: 600 11 | replicas: 2 12 | revisionHistoryLimit: 10 13 | selector: 14 | matchLabels: 15 | app: gitops-admin-plugin 16 | app.kubernetes.io/instance: gitops-admin-plugin 17 | strategy: 18 | rollingUpdate: 19 | maxSurge: 25% 20 | maxUnavailable: 25% 21 | type: RollingUpdate 22 | template: 23 | metadata: 24 | labels: 25 | app: gitops-admin-plugin 26 | app.kubernetes.io/instance: gitops-admin-plugin 27 | app.kubernetes.io/name: gitops-admin-plugin 28 | app.kubernetes.io/part-of: gitops-admin-plugin 29 | spec: 30 | containers: 31 | - image: quay.io/gnunn/gitops-admin-plugin:4.15 32 | imagePullPolicy: Always 33 | name: gitops-admin-plugin 34 | ports: 35 | - containerPort: 9443 36 | protocol: TCP 37 | resources: 38 | requests: 39 | cpu: 10m 40 | memory: 50Mi 41 | securityContext: 42 | allowPrivilegeEscalation: false 43 | capabilities: 44 | drop: 45 | - ALL 46 | terminationMessagePath: /dev/termination-log 47 | terminationMessagePolicy: File 48 | volumeMounts: 49 | - mountPath: /var/cert 50 | name: gitops-admin-plugin-cert 51 | readOnly: true 52 | - mountPath: /etc/nginx/nginx.conf 53 | name: nginx-conf 54 | readOnly: true 55 | subPath: nginx.conf 56 | dnsPolicy: ClusterFirst 57 | restartPolicy: Always 58 | schedulerName: default-scheduler 59 | securityContext: 60 | runAsNonRoot: true 61 | seccompProfile: 62 | type: RuntimeDefault 63 | terminationGracePeriodSeconds: 30 64 | volumes: 65 | - name: gitops-admin-plugin-cert 66 | secret: 67 | defaultMode: 420 68 | secretName: gitops-admin-plugin-cert 69 | - configMap: 70 | defaultMode: 420 71 | name: gitops-admin-plugin 72 | name: nginx-conf 73 | -------------------------------------------------------------------------------- /src/gitops/components/project/DestinationsList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { RowProps, TableColumn, TableData, VirtualizedTable } from '@openshift-console/dynamic-plugin-sdk'; 4 | import { sortable } from '@patternfly/react-table'; 5 | import { Destination } from '@gitops-models/AppProjectModel'; 6 | 7 | interface DestinationListProps { 8 | destinations: Destination[] 9 | } 10 | 11 | const DestinationsList: React.FC = ({ destinations }) => { 12 | 13 | if (!destinations) destinations=[] 14 | 15 | return ( 16 | 25 | ) 26 | } 27 | 28 | const destinationsListRow: React.FC> = ({ obj, activeColumnIDs }) => { 29 | 30 | return ( 31 | <> 32 | 33 | {obj.server || '-'} 34 | 35 | 36 | {obj.name} 37 | 38 | 39 | {obj.namespace} 40 | 41 | 42 | ); 43 | }; 44 | 45 | export const useDestinationsColumns = () => { 46 | 47 | const columns: TableColumn[] = React.useMemo( 48 | () => [ 49 | { 50 | title: 'Server', 51 | id: 'server', 52 | transforms: [sortable], 53 | sort: 'server', 54 | }, 55 | { 56 | title: 'Name', 57 | id: 'name', 58 | transforms: [sortable], 59 | sort: 'name', 60 | }, 61 | { 62 | title: 'Namespace', 63 | id: 'namespace', 64 | transforms: [sortable], 65 | sort: 'namespace', 66 | } 67 | ], 68 | [], 69 | ); 70 | 71 | return columns; 72 | }; 73 | 74 | export default DestinationsList; 75 | -------------------------------------------------------------------------------- /src/gitops/components/appset/AppSetNavPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { 4 | HorizontalNav, 5 | useK8sWatchResource, 6 | } from '@openshift-console/dynamic-plugin-sdk'; 7 | import { Bullseye, Spinner } from '@patternfly/react-core'; 8 | 9 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 10 | import PageTitle from '@utils/components/PageTitle/PageTitle'; 11 | import { useAppSetActionsProvider } from './hooks/useAppSetActionsProvider'; 12 | import { ApplicationSetKind, ApplicationSetModel } from '@gitops-models/ApplicationSetModel'; 13 | import ResourceYAMLTab from '@utils/components/ResourceYAMLTab/ResourceYAMLTab'; 14 | import AppSetDetailsTab from './AppSetDetailsTab'; 15 | import GeneratorsTab from './GeneratorsTab'; 16 | import AppsTab from './AppsTab'; 17 | import EventsTab from '@utils/components/EventsTab/EventsTab'; 18 | 19 | type AppSetPageProps = { 20 | name: string; 21 | namespace: string; 22 | kind: string; 23 | }; 24 | 25 | const AppSetNavPage: React.FC = ({ name, namespace, kind }) => { 26 | const { t } = useGitOpsTranslation(); 27 | const [appSet, loaded] = useK8sWatchResource({ 28 | groupVersionKind: { 29 | group: 'argoproj.io', 30 | kind: 'ApplicationSet', 31 | version: 'v1alpha1', 32 | }, 33 | kind, 34 | name, 35 | namespace, 36 | }); 37 | 38 | const [actions ] = useAppSetActionsProvider(appSet); 39 | 40 | const pages = React.useMemo( 41 | () => [ 42 | { 43 | href: '', 44 | name: t('Details'), 45 | component: AppSetDetailsTab, 46 | }, 47 | { 48 | href: 'yaml', 49 | name: t('YAML'), 50 | component: ResourceYAMLTab, 51 | }, 52 | { 53 | href: 'generators', 54 | name: t('Generators'), 55 | component: GeneratorsTab, 56 | }, 57 | { 58 | href: 'applications', 59 | name: t('Applications'), 60 | component: AppsTab, 61 | }, 62 | { 63 | href: 'events', 64 | name: t('Events'), 65 | component: EventsTab, 66 | } 67 | ], 68 | [], 69 | ); 70 | 71 | return ( 72 | <> 73 | 74 | {loaded ? ( 75 | 76 | ) : ( 77 | 78 | 79 | 80 | )} 81 | 82 | ); 83 | }; 84 | 85 | export default AppSetNavPage; 86 | -------------------------------------------------------------------------------- /src/gitops/components/appset/Generators.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { GitGeneratorFragment } from "./generators/GitGenerator"; 3 | import { GenericGeneratorFragment } from "./generators/GenericGenerator"; 4 | import { AppSetGenerator } from "@gitops-models/ApplicationSetModel"; 5 | import { ListGeneratorFragment } from "./generators/ListGenerator"; 6 | import { MatrixGeneratorFragment } from "./generators/MatrixGenerator"; 7 | import { UnionGeneratorFragment } from "./generators/UnionGenerator"; 8 | import { MergeGeneratorFragment } from "./generators/MergeGenerator"; 9 | import { ClusterGeneratorFragment } from "./generators/ClusterGenerator"; 10 | 11 | interface GeneratorsProps { 12 | generators: AppSetGenerator[] 13 | } 14 | 15 | export const Generators: React.FC = ({ generators }) => { 16 | 17 | return ( 18 |
19 | { 20 | generators.map((generator, i) => { 21 | { 22 | return ( 23 |
24 | {renderGenerator(generator)} 25 |
26 |
27 | ) 28 | } 29 | }) 30 | } 31 |
32 | ); 33 | } 34 | 35 | function renderGenerator(generator: AppSetGenerator) { 36 | var gentype = Object.keys(generator)[0]; 37 | switch (gentype) { 38 | case "clusters": 39 | return ( 40 | 41 | ) 42 | case "git": 43 | return ( 44 | 45 | ) 46 | case "list": 47 | return ( 48 | 49 | ) 50 | case "merge": 51 | return ( 52 | 53 | ) 54 | case "matrix": 55 | return ( 56 | 57 | ) 58 | // I think union is a 2,10 thing so cannot test it yet 59 | case "union": 60 | return ( 61 | 62 | ) 63 | default: 64 | return ( 65 | 66 | ) 67 | } 68 | } 69 | 70 | export default Generators; 71 | -------------------------------------------------------------------------------- /src/rollout/components/RolloutNavPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { 4 | HorizontalNav, 5 | K8sResourceCommon, 6 | useK8sWatchResource, 7 | } from '@openshift-console/dynamic-plugin-sdk'; 8 | import { Bullseye, Spinner } from '@patternfly/react-core'; 9 | 10 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 11 | import PageTitle from '@utils/components/PageTitle/PageTitle'; 12 | import { useRolloutActionsProvider } from './hooks/useRolloutActionsProvider'; 13 | import { RolloutModel } from '../models/RolloutModel'; 14 | import RolloutDetailsTab from './RolloutDetailsTab'; 15 | import RolloutPodsTab from './RolloutPodsTab'; 16 | import ResourceYAMLTab from '@utils/components/ResourceYAMLTab/ResourceYAMLTab'; 17 | import RolloutRevisionsTab from './RolloutRevisionsTab'; 18 | import EventsTab from '@utils/components/EventsTab/EventsTab'; 19 | 20 | type RolloutPageProps = { 21 | name: string; 22 | namespace: string; 23 | kind: string; 24 | }; 25 | 26 | const RolloutNavPage: React.FC = ({ name, namespace, kind }) => { 27 | const { t } = useGitOpsTranslation(); 28 | const [rollout, loaded] = useK8sWatchResource({ 29 | groupVersionKind: { 30 | group: RolloutModel.apiGroup, 31 | kind: RolloutModel.label, 32 | version: RolloutModel.apiVersion, 33 | }, 34 | kind, 35 | name, 36 | namespace, 37 | }); 38 | 39 | const [actions /*, onLazyOpen*/] = useRolloutActionsProvider(rollout); 40 | 41 | const pages = React.useMemo( 42 | () => [ 43 | { 44 | href: '', 45 | name: t('Details'), 46 | component: RolloutDetailsTab, 47 | }, 48 | { 49 | href: 'yaml', 50 | name: t('YAML'), 51 | component: ResourceYAMLTab, 52 | }, 53 | { 54 | href: 'revisions', 55 | name: t('Revisions'), 56 | component: RolloutRevisionsTab, 57 | }, 58 | { 59 | href: 'pods', 60 | name: t('Pods'), 61 | component: RolloutPodsTab, 62 | }, 63 | { 64 | href: 'events', 65 | name: t('Events'), 66 | component: EventsTab, 67 | } 68 | ], 69 | [], 70 | ); 71 | 72 | return ( 73 | <> 74 | 75 | {loaded ? ( 76 | 77 | ) : ( 78 | 79 | 80 | 81 | )} 82 | 83 | ); 84 | }; 85 | 86 | export default RolloutNavPage; 87 | -------------------------------------------------------------------------------- /src/utils/components/ModalProvider/ModalProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export type ModalComponentProps = { 4 | appendTo: () => HTMLElement; 5 | isOpen: boolean; 6 | onClose: () => void; 7 | }; 8 | export type ModalComponent = React.ComponentType; 9 | 10 | export type ModalContextType = { 11 | /** receives a modal component as an argument and injects it to the dom, the component callback will receive the following parameters, 12 | * isOpen: open state of the modal 13 | * onClose: callback to close the modal 14 | * appendTo: callback to get the dom element to append the modal to 15 | * @example 16 | * const { createModal } = useModal(); 17 | * 18 | * createModal(({ isOpen, onClose, appendTo }) => ( 19 | * 20 | * )) 21 | * 22 | */ 23 | createModal?: (modal: ModalComponent) => void; 24 | /** whether the modal is open */ 25 | isOpen?: boolean; 26 | /** the modal component to render */ 27 | modal?: ModalComponent; 28 | /** callback to close the modal */ 29 | onClose?: () => void; 30 | }; 31 | 32 | export const ModalContext = React.createContext({}); 33 | /** 34 | * A hook that returns a global modal context. This context is used to inject a modal component to the dom. 35 | * @example 36 | * const { createModal } = useModal(); 37 | * 38 | * createModal(({ isOpen, onClose, appendTo }) => ( 39 | * 40 | * )) 41 | */ 42 | export const useModal = () => React.useContext(ModalContext); 43 | 44 | export const useModalValue = (): ModalContextType => { 45 | const [modal, setModal] = React.useState(); 46 | const [isOpen, setIsOpen] = React.useState(false); 47 | 48 | const createModal = (newModal: ModalComponent) => { 49 | setIsOpen(true); 50 | setModal(() => newModal); 51 | }; 52 | 53 | const onClose = () => { 54 | setIsOpen(false); 55 | setModal(undefined); 56 | }; 57 | 58 | return { createModal, isOpen, modal, onClose }; 59 | }; 60 | 61 | export const ModalProvider: React.FC<{ value: ModalContextType }> = ({ children, value = {} }) => { 62 | const { isOpen, modal: Modal, onClose } = value; 63 | 64 | return ( 65 | 66 | {Modal && isOpen && ( 67 | document.querySelector('#modal-container')} 69 | isOpen 70 | onClose={onClose} 71 | /> 72 | )} 73 | {children} 74 | 75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /src/gitops/components/appset/AppSetDetailsTab.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { RouteComponentProps } from 'react-router'; 3 | import { 4 | DescriptionList, 5 | Grid, 6 | GridItem, 7 | PageSection, 8 | PageSectionVariants, 9 | Title 10 | } from '@patternfly/react-core'; 11 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 12 | import { ApplicationSetModel } from '@gitops-models/ApplicationSetModel'; 13 | import { getObjectModifyPermissions } from '@gitops-utils/utils'; 14 | import StandardDetailsGroup from '@utils/components/StandardDetailsGroup/StandardDetailsGroup'; 15 | import { DetailsDescriptionGroup } from '@utils/components/DetailsDescriptionGroup/DetailsDescriptionGroup'; 16 | import Status from './Status'; 17 | import { getAppSetStatus } from '@gitops-utils/gitops'; 18 | import { Conditions } from '@utils/components/Conditions/conditions'; 19 | 20 | type AppSetDetailsTabProps = RouteComponentProps<{ 21 | ns: string; 22 | name: string; 23 | }> & { 24 | obj?: any; 25 | }; 26 | 27 | const AppSetDetailsTab: React.FC = ({ obj }) => { 28 | const { t } = useGitOpsTranslation(); 29 | 30 | const [canPatch] = getObjectModifyPermissions(obj, ApplicationSetModel); 31 | 32 | return ( 33 |
34 | 35 | 36 | {t('ApplicationSet details')} 37 | 38 | 39 | 40 | 41 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | {t('Conditions')} 63 | 64 | {obj.status?.conditions ? 65 | 66 | : 67 | <>No Conditions 68 | } 69 | 70 |
71 | ); 72 | }; 73 | 74 | export default AppSetDetailsTab; 75 | -------------------------------------------------------------------------------- /src/gitops/components/project/ProjectNavPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { 4 | HorizontalNav, 5 | K8sResourceCommon, 6 | useK8sWatchResource, 7 | } from '@openshift-console/dynamic-plugin-sdk'; 8 | import { Bullseye, Spinner } from '@patternfly/react-core'; 9 | 10 | import ProjectDetailsTab from './ProjectDetailsTab'; 11 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 12 | import ProjectAllowDenyTab from './ProjectAllowDenyTab'; 13 | import ProjectRolesTab from './ProjectRolesTab'; 14 | import ProjectWindowsTab from './ProjectWindowsTab'; 15 | import ProjectAppsTab from './ProjectAppsTab'; 16 | import PageTitle from '@utils/components/PageTitle/PageTitle'; 17 | import { AppProjectModel } from '@gitops-models/AppProjectModel'; 18 | import { useProjectActionsProvider } from './hooks/useProjectActionsProvider'; 19 | import ResourceYAMLTab from '@utils/components/ResourceYAMLTab/ResourceYAMLTab'; 20 | 21 | type ProjectPageProps = { 22 | name: string; 23 | namespace: string; 24 | kind: string; 25 | }; 26 | 27 | const ProjectNavPage: React.FC = ({ name, namespace, kind }) => { 28 | const { t } = useGitOpsTranslation(); 29 | const [appProject, loaded] = useK8sWatchResource({ 30 | groupVersionKind: { 31 | group: 'argoproj.io', 32 | kind: 'AppProject', 33 | version: 'v1alpha1', 34 | }, 35 | kind, 36 | name, 37 | namespace, 38 | }); 39 | 40 | const [actions /*, onLazyOpen*/] = useProjectActionsProvider(appProject); 41 | 42 | const pages = React.useMemo( 43 | () => [ 44 | { 45 | href: '', 46 | name: t('Details'), 47 | component: ProjectDetailsTab, 48 | }, 49 | { 50 | href: 'yaml', 51 | name: t('YAML'), 52 | component: ResourceYAMLTab, 53 | }, 54 | { 55 | href: 'allowdeny', 56 | name: t('Allow/Deny'), 57 | component: ProjectAllowDenyTab, 58 | }, 59 | { 60 | href: 'roles', 61 | name: t('Roles'), 62 | component: ProjectRolesTab, 63 | }, 64 | { 65 | href: 'windows', 66 | name: t('Windows'), 67 | component: ProjectWindowsTab, 68 | }, 69 | { 70 | href: 'applications', 71 | name: t('Applications'), 72 | component: ProjectAppsTab, 73 | }, 74 | ], 75 | [], 76 | ); 77 | 78 | return ( 79 | <> 80 | 81 | {loaded ? ( 82 | 83 | ) : ( 84 | 85 | 86 | 87 | )} 88 | 89 | ); 90 | }; 91 | 92 | export default ProjectNavPage; 93 | -------------------------------------------------------------------------------- /start-console.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # Explictly set here as running with plugin-metadata.ts does not seem to set it 6 | npm_package_consolePlugin_name=gitops-admin-plugin 7 | 8 | CONSOLE_IMAGE=${CONSOLE_IMAGE:="quay.io/openshift/origin-console:latest"} 9 | CONSOLE_PORT=${CONSOLE_PORT:=9000} 10 | CONSOLE_IMAGE_PLATFORM=${CONSOLE_IMAGE_PLATFORM:="linux/amd64"} 11 | 12 | echo "Starting local OpenShift console..." 13 | 14 | BRIDGE_USER_AUTH="disabled" 15 | BRIDGE_K8S_MODE="off-cluster" 16 | BRIDGE_K8S_AUTH="bearer-token" 17 | BRIDGE_K8S_MODE_OFF_CLUSTER_SKIP_VERIFY_TLS=true 18 | BRIDGE_K8S_MODE_OFF_CLUSTER_ENDPOINT=$(oc whoami --show-server) 19 | # The monitoring operator is not always installed (e.g. for local OpenShift). Tolerate missing config maps. 20 | set +e 21 | BRIDGE_K8S_MODE_OFF_CLUSTER_THANOS=$(oc -n openshift-config-managed get configmap monitoring-shared-config -o jsonpath='{.data.thanosPublicURL}' 2>/dev/null) 22 | BRIDGE_K8S_MODE_OFF_CLUSTER_ALERTMANAGER=$(oc -n openshift-config-managed get configmap monitoring-shared-config -o jsonpath='{.data.alertmanagerPublicURL}' 2>/dev/null) 23 | set -e 24 | BRIDGE_K8S_AUTH_BEARER_TOKEN=$(oc whoami --show-token 2>/dev/null) 25 | BRIDGE_USER_SETTINGS_LOCATION="localstorage" 26 | BRIDGE_I18N_NAMESPACES="plugin__${npm_package_consolePlugin_name}" 27 | 28 | # Don't fail if the cluster doesn't have gitops. 29 | set +e 30 | GITOPS_HOSTNAME=$(oc -n openshift-gitops get route cluster -o jsonpath='{.spec.host}' 2>/dev/null) 31 | set -e 32 | if [ -n "$GITOPS_HOSTNAME" ]; then 33 | BRIDGE_K8S_MODE_OFF_CLUSTER_GITOPS="https://$GITOPS_HOSTNAME" 34 | fi 35 | 36 | echo "API Server: $BRIDGE_K8S_MODE_OFF_CLUSTER_ENDPOINT" 37 | echo "Console Image: $CONSOLE_IMAGE" 38 | echo "Console URL: http://localhost:${CONSOLE_PORT}" 39 | echo "Console Platform: $CONSOLE_IMAGE_PLATFORM" 40 | 41 | # Prefer podman if installed. Otherwise, fall back to docker. 42 | if [ -x "$(command -v podman)" ]; then 43 | if [ "$(uname -s)" = "Linux" ]; then 44 | # Use host networking on Linux since host.containers.internal is unreachable in some environments. 45 | BRIDGE_PLUGINS="${npm_package_consolePlugin_name}=http://localhost:9001" 46 | podman run --pull always --platform $CONSOLE_IMAGE_PLATFORM --rm --network=host --env-file <(set | grep BRIDGE) $CONSOLE_IMAGE 47 | else 48 | BRIDGE_PLUGINS="${npm_package_consolePlugin_name}=http://host.containers.internal:9001" 49 | podman run --pull always --platform $CONSOLE_IMAGE_PLATFORM --rm -p "$CONSOLE_PORT":9000 --env-file <(set | grep BRIDGE) $CONSOLE_IMAGE 50 | fi 51 | else 52 | BRIDGE_PLUGINS="${npm_package_consolePlugin_name}=http://host.docker.internal:9001" 53 | docker run --pull always --platform $CONSOLE_IMAGE_PLATFORM --rm -p "$CONSOLE_PORT":9000 --env-file <(set | grep BRIDGE) $CONSOLE_IMAGE 54 | fi 55 | -------------------------------------------------------------------------------- /integration-tests/tests/example-page.cy.ts: -------------------------------------------------------------------------------- 1 | import { checkErrors } from '../support'; 2 | 3 | const PLUGIN_TEMPLATE_NAME = 'console-plugin-template'; 4 | const PLUGIN_TEMPLATE_PULL_SPEC = Cypress.env('PLUGIN_TEMPLATE_PULL_SPEC'); 5 | export const isLocalDevEnvironment = 6 | Cypress.config('baseUrl').includes('localhost'); 7 | 8 | const installHelmChart = (path: string) => { 9 | cy.exec( 10 | `cd ../../console-plugin-template && ${path} upgrade -i ${PLUGIN_TEMPLATE_NAME} charts/openshift-console-plugin -n ${PLUGIN_TEMPLATE_NAME} --create-namespace --set plugin.image=${PLUGIN_TEMPLATE_PULL_SPEC}`, 11 | { 12 | failOnNonZeroExit: false, 13 | }, 14 | ) 15 | .get('[data-test="refresh-web-console"]', { timeout: 300000 }) 16 | .should('exist') 17 | .then((result) => { 18 | cy.reload(); 19 | cy.visit(`/dashboards`); 20 | cy.log('Error installing helm chart: ', result.stderr); 21 | cy.log('Successfully installed helm chart: ', result.stdout); 22 | }); 23 | }; 24 | const deleteHelmChart = (path: string) => { 25 | cy.exec( 26 | `cd ../../console-plugin-template && ${path} uninstall ${PLUGIN_TEMPLATE_NAME} -n ${PLUGIN_TEMPLATE_NAME} && oc delete namespaces ${PLUGIN_TEMPLATE_NAME}`, 27 | { 28 | failOnNonZeroExit: false, 29 | }, 30 | ).then((result) => { 31 | cy.log('Error uninstalling helm chart: ', result.stderr); 32 | cy.log('Successfully uninstalled helm chart: ', result.stdout); 33 | }); 34 | }; 35 | 36 | describe('Console plugin template test', () => { 37 | before(() => { 38 | cy.login(); 39 | 40 | if (!isLocalDevEnvironment) { 41 | console.log('this is not a local env, installig helm'); 42 | 43 | cy.exec('cd ../../console-plugin-template && ./install_helm.sh', { 44 | failOnNonZeroExit: false, 45 | }).then((result) => { 46 | cy.log('Error installing helm binary: ', result.stderr); 47 | cy.log( 48 | 'Successfully installed helm binary in "/tmp" directory: ', 49 | result.stdout, 50 | ); 51 | 52 | installHelmChart('/tmp/helm'); 53 | }); 54 | } else { 55 | console.log('this is a local env, not installing helm'); 56 | 57 | installHelmChart('helm'); 58 | } 59 | }); 60 | 61 | afterEach(() => { 62 | checkErrors(); 63 | }); 64 | 65 | after(() => { 66 | if (!isLocalDevEnvironment) { 67 | deleteHelmChart('/tmp/helm'); 68 | } else { 69 | deleteHelmChart('helm'); 70 | } 71 | cy.logout(); 72 | }); 73 | 74 | it('Verify the example page title', () => { 75 | cy.get('[data-quickstart-id="qs-nav-home"]').click(); 76 | cy.get('[data-test="nav"]').contains('Plugin Example').click(); 77 | cy.url().should('include', '/example'); 78 | cy.get('[data-test="example-page-title"]').should( 79 | 'contain', 80 | 'Hello, Plugin!', 81 | ); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/gitops/components/application/ApplicationNavPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { 4 | HorizontalNav, 5 | K8sResourceCommon, 6 | useK8sWatchResource, 7 | } from '@openshift-console/dynamic-plugin-sdk'; 8 | import { Bullseye, Spinner } from '@patternfly/react-core'; 9 | 10 | import ApplicationDetailsTab from './ApplicationDetailsTab'; 11 | import ApplicationResourcesTab from './ApplicationResourcesTab'; 12 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 13 | import ApplicationSyncStatusTab from './ApplicationSycnStatusTab'; 14 | import { useApplicationActionsProvider } from './hooks/useApplicationActionsProvider'; 15 | import PageTitle from '@utils/components/PageTitle/PageTitle'; 16 | import { ApplicationModel } from '@gitops-models/ApplicationModel'; 17 | import ResourceYAMLTab from '@utils/components/ResourceYAMLTab/ResourceYAMLTab'; 18 | import ApplicationHistoryTab from './ApplicationHistoryTab'; 19 | import EventsTab from '@utils/components/EventsTab/EventsTab'; 20 | 21 | type ApplicationPageProps = { 22 | name: string; 23 | namespace: string; 24 | kind: string; 25 | }; 26 | 27 | const ApplicationNavPage: React.FC = ({ name, namespace, kind }) => { 28 | const { t } = useGitOpsTranslation(); 29 | const [application, loaded] = useK8sWatchResource({ 30 | groupVersionKind: { 31 | group: "argoproj.io", 32 | kind: "Application", 33 | version: "v1alpha1" 34 | }, 35 | kind, 36 | name, 37 | namespace, 38 | }); 39 | 40 | const [actions ] = useApplicationActionsProvider(application); 41 | 42 | const pages = React.useMemo( 43 | () => [ 44 | { 45 | href: '', 46 | name: t('Details'), 47 | component: ApplicationDetailsTab, 48 | }, 49 | { 50 | href: 'yaml', 51 | name: t('YAML'), 52 | component: ResourceYAMLTab, 53 | }, 54 | { 55 | href: 'resources', 56 | name: t('Resources'), 57 | component: ApplicationResourcesTab, 58 | }, 59 | { 60 | href: 'syncStatus', 61 | name: t('Sync Status'), 62 | component: ApplicationSyncStatusTab, 63 | }, 64 | { 65 | href: 'History', 66 | name: t('History'), 67 | component: ApplicationHistoryTab, 68 | }, 69 | { 70 | href: 'events', 71 | name: t('Events'), 72 | component: EventsTab, 73 | } 74 | ], 75 | [], 76 | ); 77 | 78 | return ( 79 | <> 80 | 81 | {loaded ? ( 82 | 83 | ) : ( 84 | 85 | 86 | 87 | )} 88 | 89 | ); 90 | }; 91 | 92 | export default ApplicationNavPage; 93 | -------------------------------------------------------------------------------- /test/appset/matrix/union-matrix.yaml: -------------------------------------------------------------------------------- 1 | # The matrix generator can contain other combination-type generators (matrix and union). But nested matrix and union 2 | # generators cannot contain further-nested matrix or union generators. 3 | # 4 | # The generators are evaluated from most-nested to least-nested. In this case: 5 | # 1. The union generator joins two lists to make 3 parameter sets. 6 | # 2. The inner matrix generator takes the cartesian product of the two lists to make 4 parameters sets. 7 | # 3. The outer matrix generator takes the cartesian product of the 3 union and the 4 inner matrix parameter sets to 8 | # make 3*4=12 final parameter sets. 9 | # 4. The 12 final parameter sets are evaluated against the top-level template to generate 12 Applications. 10 | apiVersion: argoproj.io/v1alpha1 11 | kind: ApplicationSet 12 | metadata: 13 | name: matrix-and-union-in-matrix 14 | spec: 15 | generators: 16 | - matrix: 17 | generators: 18 | - union: 19 | mergeKeys: 20 | - cluster 21 | generators: 22 | - list: 23 | elements: 24 | - cluster: engineering-dev 25 | url: https://kubernetes.default.svc 26 | values: 27 | project: default 28 | - cluster: engineering-prod 29 | url: https://kubernetes.default.svc 30 | values: 31 | project: default 32 | - list: 33 | elements: 34 | - cluster: engineering-dev 35 | url: https://kubernetes.default.svc 36 | values: 37 | project: default 38 | - cluster: engineering-test 39 | url: https://kubernetes.default.svc 40 | values: 41 | project: default 42 | - matrix: 43 | generators: 44 | - list: 45 | elements: 46 | - values: 47 | suffix: '1' 48 | - values: 49 | suffix: '2' 50 | - list: 51 | elements: 52 | - values: 53 | prefix: 'first' 54 | - values: 55 | prefix: 'second' 56 | template: 57 | metadata: 58 | annotations: 59 | argocd.argoproj.io/skip-reconcile: "true" 60 | name: '{{values.prefix}}-{{cluster}}-{{values.suffix}}' 61 | spec: 62 | project: '{{values.project}}' 63 | source: 64 | repoURL: https://github.com/argoproj/applicationset.git 65 | targetRevision: HEAD 66 | path: '{{path}}' 67 | destination: 68 | server: '{{url}}' 69 | namespace: '{{path.basename}}' 70 | -------------------------------------------------------------------------------- /src/gitops/services/ArgoCD.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationKind, ApplicationModel, ApplicationOperation, ApplicationResourceStatus, Resource } from "@gitops-models/ApplicationModel"; 2 | import { annotationRefreshKey } from "@gitops-utils/gitops"; 3 | import { k8sPatch, k8sUpdate } from "@openshift-console/dynamic-plugin-sdk"; 4 | 5 | export const syncResourcek8s = async (app: ApplicationKind, resources: ApplicationResourceStatus[]): Promise => { 6 | 7 | var syncResources: Resource[] = []; 8 | resources.forEach(item => { 9 | const res: Resource = { name: item.name, kind: item.kind, group: item.group, namespace: item.namespace } 10 | syncResources.push(res); 11 | }); 12 | 13 | return syncAppK8s(app, syncResources); 14 | } 15 | 16 | /* 17 | * Synchronizes the application using k8s only, bypasses Argo CD RBAC and requires 18 | * the user to have patch permissions on the Application object. 19 | */ 20 | export const syncAppK8s = async (app: ApplicationKind, resources?: Resource[]): Promise => { 21 | const operation: ApplicationOperation = { 22 | info: [ 23 | { 24 | name: "Reason", 25 | value: "Initiated by user in openshift console" 26 | } 27 | ], 28 | initiatedBy: { 29 | automated: false, 30 | username: "OpenShift-Console" 31 | }, 32 | sync: {} 33 | } 34 | 35 | if (app.spec.syncPolicy) { 36 | if (app.spec.syncPolicy.retry) operation.retry = app.spec.syncPolicy.retry; 37 | if (app.spec.syncPolicy.syncOptions) operation.sync.syncOptions = app.spec.syncPolicy.syncOptions; 38 | if (app.spec.syncPolicy.automated.prune) operation.sync.prune = app.spec.syncPolicy.automated.prune; 39 | } 40 | if (resources) { 41 | operation.sync.resources = resources; 42 | } 43 | 44 | app.operation = operation; 45 | 46 | return k8sUpdate({ 47 | model: ApplicationModel, 48 | data: app, 49 | }) 50 | } 51 | 52 | export const terminateOpK8s = async(app: ApplicationKind): Promise => { 53 | return k8sPatch({ 54 | model: ApplicationModel, 55 | resource: app, 56 | data: [{ 57 | op: "replace", 58 | path: '/status/operationState/phase', 59 | value: 'Terminating', 60 | }] 61 | }) 62 | } 63 | 64 | /* 65 | * Refreshes the application using the annotation bypassing the Argo CD RBAC, see SyncApp for more info 66 | */ 67 | export const refreshAppk8s = async (app: ApplicationKind, hard: boolean): Promise => { 68 | // Note we need to add the annotations first in case it doesn't exist already 69 | 70 | 71 | if (!app.metadata.annotations) app.metadata.annotations = {}; 72 | app.metadata.annotations[annotationRefreshKey] = (hard ? "hard" : "refreshing"); 73 | 74 | return k8sUpdate({ 75 | model: ApplicationModel, 76 | data: app, 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /src/utils/components/PodList/PodRowActions.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | 4 | import { useModal } from '@utils/components/ModalProvider/ModalProvider'; 5 | import { DEFAULT_NAMESPACE } from '@gitops-utils/constants'; 6 | import { useAnnotationsModal, useK8sModel, useLabelsModal } from '@openshift-console/dynamic-plugin-sdk'; 7 | import { Dropdown, DropdownItem, KebabToggle } from '@patternfly/react-core/deprecated'; 8 | 9 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 10 | import ResourceDeleteModal from '@utils/components/ResourceDeleteModal/ResourceDeleteModal'; 11 | import { getObjectModifyPermissions, modelToRef } from '@gitops-utils/utils'; 12 | 13 | type PodRowActionsProps = { 14 | obj?: any; 15 | }; 16 | 17 | const PodRowActions: React.FC = ({ obj }) => { 18 | const { createModal } = useModal(); 19 | const history = useHistory(); 20 | const [isOpen, setIsOpen] = React.useState(false); 21 | 22 | const [podModel] = useK8sModel({kind: "Pod", version: "v1"}); 23 | 24 | const [canPatch, canUpdate, canDelete] = getObjectModifyPermissions(obj, podModel); 25 | 26 | const { t } = useGitOpsTranslation(); 27 | 28 | const launchLabelsModal = useLabelsModal(obj); 29 | const launchAnnotationsModal = useAnnotationsModal(obj); 30 | 31 | const onEditPod = () => { 32 | const cta = { 33 | href: `/k8s/ns/${obj.metadata.namespace || DEFAULT_NAMESPACE}/${modelToRef(podModel)}/${ 34 | obj.metadata.name 35 | }/yaml`, 36 | }; 37 | history.push(cta.href); 38 | }; 39 | 40 | const onDeleteModalToggle = () => { 41 | createModal(({ isOpen, onClose }) => ( 42 | 47 | )); 48 | }; 49 | 50 | const onToggle = (_event: any, isOpen: boolean) => { 51 | setIsOpen(isOpen); 52 | }; 53 | 54 | return ( 55 | setIsOpen(false)} 58 | toggle={} 59 | isOpen={isOpen} 60 | isPlain 61 | dropdownItems={[ 62 | 63 | {t('Edit labels')} 64 | , 65 | 66 | {t('Edit annotations')} 67 | , 68 | 69 | {t('Edit Pod')} 70 | , 71 | 72 | {t('Delete Pod')} 73 | , 74 | ]} 75 | /> 76 | ); 77 | }; 78 | 79 | export const getContentScrollableElement = (): HTMLElement => 80 | document.getElementById('content-scrollable'); 81 | 82 | export default PodRowActions; 83 | -------------------------------------------------------------------------------- /src/gitops/components/application/Conditions/ConditionsPopover.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Label, Popover } from '@patternfly/react-core'; 3 | import { BlueInfoCircleIcon, RedExclamationCircleIcon, StatusPopupItem, StatusPopupSection, YellowExclamationTriangleIcon } from '@openshift-console/dynamic-plugin-sdk'; 4 | 5 | import { ApplicationCondition } from '@gitops-models/ApplicationModel'; 6 | 7 | 8 | interface ConditionsPopoverProps { 9 | conditions: ApplicationCondition[]; 10 | } 11 | 12 | export const ConditionsPopover: React.FC = ({ conditions }) => { 13 | 14 | const summary:ConditionSummary = getConditionsSummary(conditions); 15 | 16 | return ( 17 | Application Conditions} 20 | bodyContent={ 21 |
22 |
23 | A list of currently observed application conditions 24 |
25 | 26 | 29 | Message 30 | 31 | } 32 | secondColumn='Type' 33 | > 34 | {conditions.map(condition => ({condition.message}))} 35 | 36 |
37 | } 38 | > 39 | 56 |
57 | ) 58 | }; 59 | 60 | type ConditionSummary = { 61 | error: number, 62 | warning: number, 63 | info: number 64 | } 65 | 66 | function getConditionsSummary(conditions:ApplicationCondition[]): ConditionSummary { 67 | 68 | var summary: ConditionSummary = { 69 | error: 0, 70 | warning: 0, 71 | info: 0 72 | }; 73 | 74 | conditions.map(condition => { 75 | 76 | if (condition.type.endsWith("Error")) summary.error++ 77 | else if (condition.type.endsWith("Warn")) summary.warning++ 78 | else summary.info++; 79 | }); 80 | 81 | return summary; 82 | } 83 | -------------------------------------------------------------------------------- /src/gitops/components/application/ResourceRowActions.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ApplicationKind, ApplicationResourceStatus } from '@gitops-models/ApplicationModel'; 3 | import { syncResourcek8s } from '@gitops-services/ArgoCD'; 4 | import { useK8sModel, k8sGet } from '@openshift-console/dynamic-plugin-sdk'; 5 | import { Dropdown, DropdownItem, KebabToggle } from '@patternfly/react-core/deprecated'; 6 | import { useModal } from '@utils/components/ModalProvider/ModalProvider' 7 | import ResourceDeleteModal from '@utils/components/ResourceDeleteModal/ResourceDeleteModal'; 8 | 9 | type ResourceRowActionsProps = { 10 | resource: ApplicationResourceStatus; 11 | application: ApplicationKind; 12 | argoBaseURL: string; 13 | }; 14 | 15 | function getResourceURL(argoBaseURL: string, resource: ApplicationResourceStatus): string { 16 | return argoBaseURL+"?resource=&node=" + encodeURI((resource.group?resource.group:"") + "/" + resource.kind + "/" + (resource.namespace?resource.namespace:"") + "/" + resource.name); 17 | } 18 | 19 | const ResourceRowActions: React.FC = ({ resource, application, argoBaseURL }) => { 20 | const [isOpen, setIsOpen] = React.useState(false); 21 | const [model] = useK8sModel({ group: resource.group, version: resource.version, kind: resource.kind }); 22 | 23 | const { createModal } = useModal(); 24 | 25 | const getObject = () => 26 | k8sGet({ 27 | model: model, 28 | name: resource.name, 29 | ns: resource.namespace, 30 | }); 31 | 32 | 33 | 34 | const onViewResource = () => { 35 | window.open(getResourceURL(argoBaseURL, resource), '_blank'); 36 | }; 37 | 38 | const onSyncResource = () => { 39 | syncResourcek8s(application, [resource]) 40 | }; 41 | 42 | const onToggle = (_event: any, isOpen: boolean) => { 43 | setIsOpen(isOpen); 44 | }; 45 | 46 | const onDeleteResource = async () => { 47 | const obj = await getObject(); 48 | createModal(({ isOpen, onClose }) => ( 49 | 54 | )); 55 | }; 56 | 57 | return ( 58 | setIsOpen(false)} 61 | toggle={} 62 | isOpen={isOpen} 63 | isPlain 64 | dropdownItems={[ 65 | 66 | {'Details'} 67 | , 68 | 69 | {'Sync'} 70 | , 71 | 72 | {'Delete'} 73 | 74 | ]} 75 | /> 76 | ); 77 | }; 78 | 79 | export const getContentScrollableElement = (): HTMLElement => 80 | document.getElementById('content-scrollable'); 81 | 82 | export default ResourceRowActions; 83 | -------------------------------------------------------------------------------- /src/gitops/components/application/History/History.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { ApplicationHistory } from '@gitops-models/ApplicationModel'; 4 | import { RowProps, TableColumn, TableData, Timestamp, VirtualizedTable } from '@openshift-console/dynamic-plugin-sdk'; 5 | import { sortable } from '@patternfly/react-table'; 6 | import Revision from '../Revision/Revision'; 7 | 8 | import './History.scss'; 9 | 10 | interface HistoryListProps { 11 | history: ApplicationHistory[] 12 | } 13 | 14 | const HistoryList: React.FC = ({ history }) => { 15 | 16 | return ( 17 | <> 18 | 26 | 27 | ) 28 | } 29 | 30 | const historyListRow: React.FC> = ({ obj, activeColumnIDs }) => { 31 | 32 | return ( 33 | <> 34 | 35 | {obj.id} 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 49 | 50 | 51 | ); 52 | }; 53 | 54 | export const useHistoryColumns = () => { 55 | 56 | const columns: TableColumn[] = React.useMemo( 57 | () => [ 58 | { 59 | title: 'ID', 60 | id: 'id', 61 | transforms: [sortable], 62 | sort: `id`, 63 | props: { className: 'gitops-admin-plugin__history-id-column' } 64 | }, 65 | { 66 | title: 'Deploy Started At', 67 | id: 'deployStartedAt', 68 | transforms: [sortable], 69 | sort: `deployStartedAt` 70 | }, 71 | { 72 | title: 'Deployed At', 73 | id: 'deployedAt', 74 | transforms: [sortable], 75 | sort: 'deployedAt' 76 | }, 77 | { 78 | title: 'Revision', 79 | id: 'revision', 80 | transforms: [sortable], 81 | sort: 'revision' 82 | } 83 | // { 84 | // title: 'Source', 85 | // sort: 'source', 86 | // id: 'source', 87 | // transforms: [] 88 | // } 89 | ], 90 | [], 91 | ); 92 | 93 | return columns; 94 | }; 95 | 96 | export default HistoryList; 97 | -------------------------------------------------------------------------------- /src/utils/components/Conditions/conditions.tsx: -------------------------------------------------------------------------------- 1 | // Stolen from OpenShift Console code: https://github.com/openshift/console/blob/db079a83c63a75ad360e241d07ec6037d0f6e1b9/frontend/public/components/conditions.tsx#L15 2 | import * as React from 'react'; 3 | 4 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 5 | import { CamelCaseWrap, Timestamp } from '@openshift-console/dynamic-plugin-sdk'; 6 | 7 | export const Conditions: React.FC = ({ 8 | conditions 9 | }) => { 10 | const { t } = useGitOpsTranslation(); 11 | 12 | const getStatusLabel = (status: string) => { 13 | switch (status) { 14 | case 'True': 15 | return t('public~True'); 16 | case 'False': 17 | return t('public~False'); 18 | default: 19 | return status; 20 | } 21 | }; 22 | 23 | const rows = (conditions)?.map?.( 24 | (condition: any, i: number) => ( 25 |
30 | <> 31 |
32 | 33 |
34 |
35 | {getStatusLabel(condition.status)} 36 |
37 | 38 |
42 | 43 |
44 |
45 | 46 |
47 | {/* remove initial newline which appears in route messages */} 48 |
52 | {condition.message?.trim() || '-'} 53 |
54 |
55 | ), 56 | ); 57 | 58 | return ( 59 | <> 60 | {conditions?.length ? ( 61 |
62 |
63 | <> 64 |
{t('public~Type')}
65 |
{t('public~Status')}
66 | 67 |
{t('public~Updated')}
68 |
{t('public~Reason')}
69 |
{t('public~Message')}
70 |
71 |
{rows}
72 |
73 | ) : ( 74 |
75 |
{t('public~No conditions found')}
76 |
77 | )} 78 | 79 | ); 80 | }; 81 | Conditions.displayName = 'Conditions'; 82 | 83 | export type ConditionsProps = { 84 | conditions: any; 85 | title?: string; 86 | subTitle?: string; 87 | }; 88 | -------------------------------------------------------------------------------- /src/gitops/components/appset/hooks/useAppSetActionsProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | 4 | import { useModal } from '@utils/components/ModalProvider/ModalProvider'; 5 | import { Action, K8sVerb, useAnnotationsModal, useLabelsModal } from '@openshift-console/dynamic-plugin-sdk'; 6 | 7 | import ResourceDeleteModal from '@utils/components/ResourceDeleteModal/ResourceDeleteModal'; 8 | import { ApplicationSetKind, ApplicationSetModel, applicationSetModelRef } from '@gitops-models/ApplicationSetModel'; 9 | 10 | type UseAppSetActionsProvider = ( 11 | appSet: ApplicationSetKind, 12 | ) => [actions: Action[] /*, onOpen: () => void*/]; 13 | const t = (key: string) => key; 14 | 15 | export const useAppSetActionsProvider: UseAppSetActionsProvider = (appSet) => { 16 | const { createModal } = useModal(); 17 | const history = useHistory(); 18 | 19 | const launchLabelsModal = useLabelsModal(appSet); 20 | const launchAnnotationsModal = useAnnotationsModal(appSet); 21 | 22 | const actions = React.useMemo( 23 | () => [ 24 | { 25 | id: 'dataimportcron-action-edit-labels', 26 | disabled: false, 27 | label: t('Edit labels'), 28 | accessReview: { 29 | group: ApplicationSetModel.apiGroup, 30 | verb: 'patch' as K8sVerb, 31 | resource: ApplicationSetModel.plural, 32 | namespace: appSet?.metadata?.namespace 33 | }, 34 | cta: () => {launchLabelsModal()} 35 | }, 36 | { 37 | id: 'crontab-action-edit-annotations', 38 | disabled: false, 39 | label: t('Edit annotations'), 40 | accessReview: { 41 | group: ApplicationSetModel.apiGroup, 42 | verb: 'patch' as K8sVerb, 43 | resource: ApplicationSetModel.plural, 44 | namespace: appSet?.metadata?.namespace 45 | }, 46 | cta: () => {launchAnnotationsModal()} 47 | }, 48 | { 49 | id: 'crontab-action-edit-crontab', 50 | disabled: false, 51 | label: t('Edit'), 52 | accessReview: { 53 | group: ApplicationSetModel.apiGroup, 54 | verb: 'update' as K8sVerb, 55 | resource: ApplicationSetModel.plural, 56 | namespace: appSet?.metadata?.namespace 57 | }, 58 | cta: () => 59 | history.push( 60 | `/k8s/ns/${appSet.metadata.namespace}/${applicationSetModelRef}/${appSet.metadata.name}/yaml`, 61 | ), 62 | }, 63 | { 64 | id: 'crontab-action-delete', 65 | label: t('Delete'), 66 | accessReview: { 67 | group: ApplicationSetModel.apiGroup, 68 | verb: 'delete' as K8sVerb, 69 | resource: ApplicationSetModel.plural, 70 | namespace: appSet?.metadata?.namespace 71 | }, 72 | cta: () => 73 | createModal(({ isOpen, onClose }) => ( 74 | 80 | )), 81 | // ,accessReview: asAccessReview(DataImportCronModel, cronTab, 'delete'), 82 | }, 83 | ], 84 | [/*t, */ appSet, createModal /*, dataSource*/, history], 85 | ); 86 | 87 | return [actions]; 88 | }; 89 | -------------------------------------------------------------------------------- /src/gitops/components/project/hooks/useProjectActionsProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | 4 | import { AppProjectKind, AppProjectModel, appProjectModelRef } from '@gitops-models/AppProjectModel'; 5 | import { useModal } from '@utils/components/ModalProvider/ModalProvider'; 6 | import { Action, K8sVerb, useAnnotationsModal, useLabelsModal } from '@openshift-console/dynamic-plugin-sdk'; 7 | 8 | import ResourceDeleteModal from '@utils/components/ResourceDeleteModal/ResourceDeleteModal'; 9 | 10 | type UseProjectActionsProvider = ( 11 | appProject: AppProjectKind, 12 | ) => [actions: Action[] /*, onOpen: () => void*/]; 13 | const t = (key: string) => key; 14 | 15 | export const useProjectActionsProvider: UseProjectActionsProvider = (appProject) => { 16 | const { createModal } = useModal(); 17 | const history = useHistory(); 18 | 19 | const launchLabelsModal = useLabelsModal(appProject); 20 | const launchAnnotationsModal = useAnnotationsModal(appProject); 21 | 22 | const actions = React.useMemo( 23 | () => [ 24 | { 25 | id: 'dataimportcron-action-edit-labels', 26 | disabled: false, 27 | label: t('Edit labels'), 28 | accessReview: { 29 | group: AppProjectModel.apiGroup, 30 | verb: 'patch' as K8sVerb, 31 | resource: AppProjectModel.plural, 32 | namespace: appProject?.metadata?.namespace 33 | }, 34 | cta: () => {launchLabelsModal()} 35 | }, 36 | { 37 | id: 'crontab-action-edit-annotations', 38 | disabled: false, 39 | label: t('Edit annotations'), 40 | accessReview: { 41 | group: AppProjectModel.apiGroup, 42 | verb: 'patch' as K8sVerb, 43 | resource: AppProjectModel.plural, 44 | namespace: appProject?.metadata?.namespace 45 | }, 46 | cta: () => {launchAnnotationsModal()} 47 | }, 48 | { 49 | id: 'crontab-action-edit-crontab', 50 | disabled: false, 51 | label: t('Edit'), 52 | accessReview: { 53 | group: AppProjectModel.apiGroup, 54 | verb: 'update' as K8sVerb, 55 | resource: AppProjectModel.plural, 56 | namespace: appProject?.metadata?.namespace 57 | }, 58 | cta: () => 59 | history.push( 60 | `/k8s/ns/${appProject.metadata.namespace}/${appProjectModelRef}/${appProject.metadata.name}/yaml`, 61 | ), 62 | }, 63 | { 64 | id: 'crontab-action-delete', 65 | label: t('Delete'), 66 | accessReview: { 67 | group: AppProjectModel.apiGroup, 68 | verb: 'delete' as K8sVerb, 69 | resource: AppProjectModel.plural, 70 | namespace: appProject?.metadata?.namespace 71 | }, 72 | cta: () => 73 | createModal(({ isOpen, onClose }) => ( 74 | 80 | )), 81 | // ,accessReview: asAccessReview(DataImportCronModel, cronTab, 'delete'), 82 | }, 83 | ], 84 | [/*t, */ appProject, createModal /*, dataSource*/, history], 85 | ); 86 | 87 | return [actions]; 88 | }; 89 | -------------------------------------------------------------------------------- /src/gitops/models/ApplicationSetModel.ts: -------------------------------------------------------------------------------- 1 | import { modelToRef } from '@gitops-utils/utils'; 2 | import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; 3 | import { K8sResourceCondition } from '@openshift-console/dynamic-plugin-sdk-internal/lib/extensions/console-types'; 4 | import { K8sModel, Selector } from '@openshift-console/dynamic-plugin-sdk/lib/api/common-types'; 5 | 6 | export const ApplicationSetModel: K8sModel = { 7 | label: 'ApplicationSet', 8 | labelPlural: 'ApplicationSets', 9 | apiVersion: 'v1alpha1', 10 | apiGroup: 'argoproj.io', 11 | plural: 'applicationsets', 12 | abbr: 'appset', 13 | namespaced: true, 14 | kind: 'ApplicationSet', 15 | id: 'applicationset', 16 | crd: true, 17 | }; 18 | 19 | export type ListAppSetGenerator = { 20 | elements?: Object[] 21 | elementsYaml?: string 22 | } 23 | 24 | export type ClusterAppSetGenerator = { 25 | selector?: Selector, 26 | values?: Map 27 | } 28 | 29 | export type GeneratorParent = { 30 | generators: AppSetGenerator[] 31 | } 32 | 33 | export type MatrixAppSetGenerator = GeneratorParent & { 34 | } 35 | 36 | export type UnionAppSetGenerator = GeneratorParent & { 37 | } 38 | 39 | export type MergeAppSetGenerator = GeneratorParent & { 40 | mergeKeys: string[] 41 | } 42 | 43 | export type GitAppSetGenerator = { 44 | repoURL: string, 45 | revision?: string, 46 | files?: { 47 | path: string 48 | }[], 49 | directories?: { 50 | exclude: boolean, 51 | path: string 52 | }[] 53 | } 54 | 55 | export type SCMProviderAppSetGenerator = { 56 | awsCodeCommit?: Object, 57 | azureDevOps?: Object, 58 | bitbucket?: Object, 59 | bitbucketServer?: Object, 60 | cloneProtocol?: string, 61 | filters?: Object[], 62 | gitea?: Object, 63 | github?: Object, 64 | gitlab?: Object, 65 | requeueAfterSeconds?: number, 66 | values?: Map 67 | } 68 | 69 | export type PullRequestAppSetGenerator = { 70 | azuredevops?: Object, 71 | bitbucket?: Object, 72 | bitbucketServer?: Object, 73 | filters?: Object[], 74 | gitea?: Object, 75 | github?: Object, 76 | gitlab?: Object, 77 | requeueAfterSeconds?: number 78 | } 79 | 80 | export type ClusterDecisionresource = { 81 | configMapRef: string, 82 | labelSelector: Object, 83 | name: string, 84 | requeueAfterSeconds?: number 85 | values?: Map 86 | } 87 | 88 | export type AppSetGenerator = { 89 | clusters?: ClusterAppSetGenerator 90 | git?: GitAppSetGenerator, 91 | list?: ListAppSetGenerator, 92 | matrix?: MatrixAppSetGenerator, 93 | merge?: MergeAppSetGenerator, 94 | pullRequest: PullRequestAppSetGenerator, 95 | scmProvider?: SCMProviderAppSetGenerator, 96 | union?: UnionAppSetGenerator 97 | } 98 | 99 | export type ApplicationSetSpec = GeneratorParent & { 100 | } 101 | 102 | export type ApplicationSetStatus = { 103 | conditions?: K8sResourceCondition[] 104 | } 105 | 106 | export type ApplicationSetKind = K8sResourceCommon & { 107 | spec: ApplicationSetSpec; 108 | status?: ApplicationSetStatus; 109 | }; 110 | 111 | export const applicationSetModelRef = modelToRef(ApplicationSetModel); 112 | -------------------------------------------------------------------------------- /src/utils/components/ResourceDeleteModal/ResourceDeleteModal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Alert, Button, Modal, ModalVariant, Text } from '@patternfly/react-core'; 3 | import * as _ from 'lodash'; 4 | import { K8sResourceCommon, getGroupVersionKindForResource, k8sDelete, useK8sModel } from '@openshift-console/dynamic-plugin-sdk'; 5 | import { useNavigate } from 'react-router-dom-v5-compat'; 6 | import { useLastNamespace } from '@openshift-console/dynamic-plugin-sdk-internal'; 7 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 8 | import { getResourceUrl } from '@gitops-utils/utils'; 9 | 10 | type ResourceDeleteModalProps = { 11 | isOpen: boolean; 12 | resource: K8sResourceCommon; 13 | onClose: () => void; 14 | btnText?: string; 15 | shouldRedirect?: boolean; 16 | }; 17 | 18 | const ResourceDeleteModal = (props: ResourceDeleteModalProps) => { 19 | const { resource, btnText, shouldRedirect, isOpen, onClose } = props; 20 | const [error, setError] = React.useState(null); 21 | 22 | const [isChecked, setIsChecked] = React.useState(true); 23 | 24 | const [model] = useK8sModel(getGroupVersionKindForResource(resource)); 25 | 26 | const [lastNamespace] = useLastNamespace(); 27 | const navigate = useNavigate(); 28 | const { t } = useGitOpsTranslation(); 29 | 30 | const submit = (event) => { 31 | event.preventDefault(); 32 | const propagationPolicy = isChecked && model ? null : 'Orphan'; 33 | 34 | const json = propagationPolicy 35 | ? { kind: 'DeleteOptions', apiVersion: 'v1', propagationPolicy } 36 | : null; 37 | 38 | k8sDelete({ model, resource, json }) 39 | .then(() => { 40 | const url = getResourceUrl({ model, activeNamespace: lastNamespace }); 41 | onClose(); 42 | shouldRedirect && navigate(url); 43 | }) 44 | .catch((err) => { 45 | setError(err.message); 46 | }); 47 | }; 48 | 49 | return ( 50 | 59 | {btnText || 'Delete'} 60 | , 61 | , 64 | ]} 65 | > 66 | 67 | Are you sure you want to delete{' '} 68 | { resource.metadata.name }? 69 | 70 |
71 | 79 |
80 | {error && ( 81 | 87 |
{error}
88 |
89 | )} 90 |
91 | ); 92 | }; 93 | 94 | export default ResourceDeleteModal; 95 | -------------------------------------------------------------------------------- /src/gitops/components/application/Statuses/OperationState.tsx: -------------------------------------------------------------------------------- 1 | // Adapted from Argo CD UI code here: 2 | 3 | import { ApplicationKind } from "@gitops-models/ApplicationModel"; 4 | import { PhaseStatus } from "src/gitops/utils/constants"; 5 | import * as React from "react"; 6 | import { PhaseErrorIcon, PhaseFailedIcon, PhaseRunningIcon, PhaseSucceededIcon, PhaseTerminatingIcon } from "@utils/components/Icons/Icons"; 7 | import { getAppOperationState, getOperationType } from "@gitops-utils/gitops"; 8 | 9 | interface OperationStateProps { 10 | app: ApplicationKind; 11 | quiet?: boolean; 12 | } 13 | 14 | // Adapted from Argo CD UI code here: 15 | // https://github.com/argoproj/argo-cd/blob/master/ui/src/app/applications/components/utils.tsx 16 | export const OperationState: React.FC = ({ app, quiet }) => { 17 | 18 | const appOperationState = getAppOperationState(app); 19 | if (appOperationState === undefined) { 20 | return ; 21 | } 22 | if (quiet && [PhaseStatus.RUNNING, PhaseStatus.FAILED, PhaseStatus.ERROR].indexOf(appOperationState.phase) === -1) { 23 | return ; 24 | } 25 | 26 | let targetIcon: React.ReactNode; 27 | const phase = appOperationState == undefined ? PhaseStatus.RUNNING: appOperationState.phase; 28 | switch (phase) { 29 | case PhaseStatus.FAILED: 30 | targetIcon = ; 31 | break; 32 | case PhaseStatus.ERROR: 33 | targetIcon = ; 34 | break; 35 | case PhaseStatus.RUNNING: 36 | targetIcon = ; 37 | break; 38 | case PhaseStatus.SUCCEEDED: 39 | targetIcon = ; 40 | break; 41 | case PhaseStatus.TERMINATING: 42 | targetIcon = ; 43 | break; 44 | default: 45 | targetIcon = ; 46 | } 47 | 48 | return ( 49 | 50 | {targetIcon} {getOperationStateTitle(app)} 51 | 52 | ); 53 | }; 54 | 55 | // Adapted from Argo CD UI code here: 56 | // https://github.com/argoproj/argo-cd/blob/master/ui/src/app/applications/components/utils.tsx 57 | const getOperationStateTitle = (app: ApplicationKind) => { 58 | if (!app.status && !app.status.operationState) { 59 | return 'Unknown'; 60 | } 61 | 62 | const operationState = getAppOperationState(app); 63 | const operationType = getOperationType(app); 64 | switch (operationType) { 65 | case 'Delete': 66 | return 'Deleting'; 67 | case 'Sync': 68 | if (operationState != undefined) { 69 | switch (operationState.phase) { 70 | case PhaseStatus.RUNNING: 71 | return 'Syncing'; 72 | case PhaseStatus.ERROR: 73 | return 'Sync error'; 74 | case PhaseStatus.FAILED: 75 | return 'Sync failed'; 76 | case PhaseStatus.SUCCEEDED: 77 | return 'Sync OK'; 78 | case PhaseStatus.TERMINATING: 79 | return 'Terminated'; 80 | } 81 | } 82 | } 83 | return 'Unknown'; 84 | }; 85 | -------------------------------------------------------------------------------- /src/gitops/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { K8sResourceCommon, K8sVerb, useAccessReview } from '@openshift-console/dynamic-plugin-sdk'; 2 | import { K8sModel } from '@openshift-console/dynamic-plugin-sdk/lib/api/common-types'; 3 | 4 | const modelToRef = (obj: K8sModel) => `${obj.apiGroup}~${obj.apiVersion}~${obj.kind}`; 5 | const modelToGroupVersionKind = (obj: K8sModel) => ({ 6 | version: obj.apiVersion, 7 | kind: obj.kind, 8 | group: obj.apiGroup, 9 | }); 10 | 11 | export { modelToGroupVersionKind, modelToRef }; 12 | 13 | export type ResourceUrlProps = { 14 | model: K8sModel; 15 | resource?: K8sResourceCommon; 16 | activeNamespace?: string; 17 | name?: string; 18 | }; 19 | 20 | export const ALL_NAMESPACES_SESSION_KEY = '#ALL_NS#'; 21 | 22 | /** 23 | * function for getting a resource URL 24 | * @param {ResourceUrlProps} urlProps - object with model, resource to get the URL from (optional) and active namespace/project name (optional) 25 | * @returns {string} the URL for the resource 26 | */ 27 | export const getResourceUrl = (urlProps: ResourceUrlProps): string => { 28 | const { activeNamespace, model, resource } = urlProps; 29 | 30 | if (!model) return null; 31 | const { crd, namespaced, plural } = model; 32 | 33 | const namespace = 34 | resource?.metadata?.namespace || 35 | (activeNamespace !== ALL_NAMESPACES_SESSION_KEY && activeNamespace); 36 | const namespaceUrl = namespace ? `ns/${namespace}` : 'all-namespaces'; 37 | 38 | const ref = crd ? `${model.apiGroup || 'core'}~${model.apiVersion}~${model.kind}` : plural || ''; 39 | const name = resource?.metadata?.name || ''; 40 | 41 | return `/k8s/${namespaced ? namespaceUrl : 'cluster'}/${ref}/${name}`; 42 | }; 43 | 44 | export function getObjectModifyPermissions(obj: K8sResourceCommon, model: K8sModel): [[boolean, boolean], [boolean, boolean], [boolean, boolean]] { 45 | 46 | const canPatch = useAccessReview({ 47 | group: model.apiGroup, 48 | verb: 'patch' as K8sVerb, 49 | resource: model.plural, 50 | name: obj.metadata.name, 51 | namespace: obj.metadata.namespace, 52 | }); 53 | 54 | const canUpdate = useAccessReview({ 55 | group: model.apiGroup, 56 | verb: 'update' as K8sVerb, 57 | resource: model.plural, 58 | name: obj.metadata.name, 59 | namespace: obj.metadata.namespace, 60 | }); 61 | 62 | const canDelete = useAccessReview({ 63 | group: model.apiGroup, 64 | verb: 'delete' as K8sVerb, 65 | resource: model.plural, 66 | name: obj.metadata.name, 67 | namespace: obj.metadata.namespace, 68 | }); 69 | 70 | return [canPatch, canUpdate, canDelete]; 71 | } 72 | 73 | export function resourceAsArray(resource: K8sResourceCommon | K8sResourceCommon[]): K8sResourceCommon[] { 74 | return Array.isArray(resource) ? resource : [resource]; 75 | } 76 | 77 | export function encodeHTMLEntities(rawStr: string): string { 78 | if (rawStr == undefined) return undefined; 79 | return rawStr.replace(/[\u00A0-\u9999<>\&]/g, ((i) => `&#${i.charCodeAt(0)};`)); 80 | } 81 | 82 | export function getSelectorSearchURL(namespace: string, kind: string, selector: string): string { 83 | if (namespace) { 84 | return "/search/ns/" + namespace + "?kind=" + kind + "&q=" + selector; 85 | } else { 86 | return "/search?kind=" + kind + "&q=" + selector; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/gitops/components/project/ProjectRolesTab.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { RowProps, TableColumn, TableData, VirtualizedTable } from '@openshift-console/dynamic-plugin-sdk'; 4 | import { RouteComponentProps } from 'react-router'; 5 | import { sortable } from '@patternfly/react-table'; 6 | import { List, ListItem, PageSection, PageSectionVariants } from '@patternfly/react-core'; 7 | import { AppProjectKind, Role } from '@gitops-models/AppProjectModel'; 8 | 9 | type ProjectRolesProps = RouteComponentProps<{ 10 | ns: string; 11 | name: string; 12 | }> & { 13 | obj?: AppProjectKind; 14 | }; 15 | 16 | const ProjectRolesTab: React.FC = ({ obj }) => { 17 | 18 | //const { t } = useGitOpsTranslation(); 19 | 20 | var roles: Role[]; 21 | if (obj?.spec?.roles) { 22 | roles = obj.spec.roles; 23 | } else { 24 | roles = []; 25 | } 26 | 27 | return ( 28 |
29 | 30 | 38 | 39 |
40 | ) 41 | } 42 | 43 | const rolesListRow: React.FC> = ({ obj, activeColumnIDs }) => { 44 | 45 | return ( 46 | <> 47 | 48 | {obj.name} 49 | 50 | 51 | {obj.description} 52 | 53 | 54 | {obj.groups && 55 | 56 | {obj.groups.map(el=> {el})} 57 | 58 | } 59 | 60 | 61 | {obj.policies && 62 | 63 | {obj.policies.map(el=> {el})} 64 | 65 | } 66 | 67 | 68 | ); 69 | }; 70 | 71 | export const useRolesColumns = () => { 72 | 73 | const columns: TableColumn[] = React.useMemo( 74 | () => [ 75 | { 76 | title: 'Name', 77 | id: 'name', 78 | transforms: [sortable], 79 | sort: `name` 80 | }, 81 | { 82 | title: 'Description', 83 | id: 'description', 84 | transforms: [sortable], 85 | sort: `description` 86 | }, 87 | { 88 | title: 'Groups', 89 | id: 'groups' 90 | }, 91 | { 92 | title: 'Policies', 93 | id: 'policies' 94 | } 95 | ], 96 | [], 97 | ); 98 | 99 | return columns; 100 | }; 101 | 102 | export default ProjectRolesTab; 103 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitops-admin-plugin", 3 | "version": "0.2.0", 4 | "private": true, 5 | "repository": "git@github.com:gnunn-gitops/gitops-console-plugin.git", 6 | "license": "Apache-2.0", 7 | "scripts": { 8 | "clean": "rm -rf dist", 9 | "build": "yarn clean && NODE_ENV=production yarn ts-node node_modules/.bin/webpack", 10 | "build-dev": "yarn clean && yarn ts-node node_modules/.bin/webpack", 11 | "start": "yarn ts-node node_modules/.bin/webpack serve", 12 | "start-console": "./start-console.sh", 13 | "i18n": "./i18n-scripts/build-i18n.sh && node ./i18n-scripts/set-english-defaults.js", 14 | "ts-node": "ts-node -O '{\"module\":\"commonjs\"}'", 15 | "lint": "eslint ./src ./integration-tests --fix && stylelint \"src/**/*.css\" --allow-empty-input --fix", 16 | "test-cypress": "cd integration-tests s && cypress open --env openshift=true", 17 | "test-cypress-headless": "cd integration-tests && node --max-old-space-size=4096 ../node_modules/.bin/cypress run --env openshift=true --browser ${BRIDGE_E2E_BROWSER_NAME:=electron}", 18 | "cypress-merge": "mochawesome-merge ./integration-tests/screenshots/cypress_report*.json > ./integration-tests/screenshots/cypress.json", 19 | "cypress-generate": "marge -o ./integration-tests/screenshots/ -f cypress-report -t 'OpenShift Console Plugin Template Cypress Test Results' -p 'OpenShift Cypress Plugin Template Test Results' --showPassed false --assetsDir ./integration-tests/screenshots/cypress/assets ./integration-tests/screenshots/cypress.json", 20 | "cypress-postreport": "yarn cypress-merge && yarn cypress-generate" 21 | }, 22 | "devDependencies": { 23 | "@cypress/webpack-preprocessor": "^5.15.5", 24 | "@openshift-console/dynamic-plugin-sdk": "1.1.0", 25 | "@openshift-console/dynamic-plugin-sdk-internal": "1.0.0", 26 | "@openshift-console/dynamic-plugin-sdk-webpack": "1.0.2", 27 | "@openshift-console/plugin-shared": "^0.0.1", 28 | "@patternfly/react-core": "5.1.1", 29 | "@patternfly/react-icons": "5.1.1", 30 | "@patternfly/react-styles": "5.1.1", 31 | "@patternfly/react-table": "5.1.1", 32 | "@types/node": "^18.0.0", 33 | "@types/react": "^17.0.1", 34 | "@types/react-helmet": "^6.1.4", 35 | "@typescript-eslint/eslint-plugin": "^5.14.0", 36 | "@typescript-eslint/parser": "^5.14.0", 37 | "copy-webpack-plugin": "^6.4.1", 38 | "css-loader": "^6.7.1", 39 | "cypress": "^12.17.4", 40 | "cypress-multi-reporters": "^1.6.2", 41 | "eslint": "^8.10.0", 42 | "eslint-config-prettier": "^8.5.0", 43 | "eslint-plugin-cypress": "^2.12.1", 44 | "eslint-plugin-prettier": "^4.0.0", 45 | "eslint-plugin-react": "^7.29.1", 46 | "git-url-parse": "^13.1.0", 47 | "i18next-parser": "^3.11.0", 48 | "mocha-junit-reporter": "^2.2.0", 49 | "mochawesome": "^7.1.3", 50 | "mochawesome-merge": "^4.3.0", 51 | "pluralize": "^8.0.0", 52 | "prettier": "^2.7.1", 53 | "prettier-stylelint": "^0.4.2", 54 | "react": "^17.0.1", 55 | "react-dom": "^17.0.1", 56 | "react-helmet": "^6.1.0", 57 | "react-i18next": "^11.7.3", 58 | "react-router": "5.3.x", 59 | "react-router-dom-v5-compat": "^6.22.0", 60 | "react-router-dom": "5.3.x", 61 | "resolve-url-loader": "^5.0.0", 62 | "sass": "^1.49.7", 63 | "sass-loader": "^12.4.0", 64 | "style-loader": "^3.3.1", 65 | "stylelint": "^15.3.0", 66 | "stylelint-config-standard": "^31.0.0", 67 | "ts-loader": "^9.3.1", 68 | "ts-node": "^10.8.1", 69 | "tsconfig-paths-webpack-plugin": "^4.1.0", 70 | "typescript": "^4.7.4", 71 | "webpack": "5.75.0", 72 | "webpack-cli": "^4.9.2", 73 | "webpack-dev-server": "^4.7.4" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/gitops/components/application/Sources/Sources.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { ApplicationSource } from '@gitops-models/ApplicationModel'; 4 | import { RowProps, TableColumn, TableData, VirtualizedTable } from '@openshift-console/dynamic-plugin-sdk'; 5 | import { sortable } from '@patternfly/react-table'; 6 | import ExternalLink from '@gitops-shared/ExternalLink'; 7 | 8 | import './Sources.scss'; 9 | import { repoUrl, revisionUrl } from '@gitops-utils/urls'; 10 | 11 | interface SourceListProps { 12 | sources: ApplicationSource[] 13 | } 14 | 15 | const SourceList: React.FC = ({ sources }) => { 16 | 17 | return ( 18 | <> 19 | 27 | 28 | ) 29 | } 30 | 31 | const sourceListRow: React.FC> = ({ obj, activeColumnIDs }) => { 32 | 33 | return ( 34 | <> 35 | 36 | {(obj.chart 37 | 38 | 39 | 40 | {repoUrl(obj.repoURL)} 41 | 42 | 43 | 44 | {obj.chart || '-'} 45 | 46 | 47 | {obj.chart ? 48 | obj.path 49 | : 50 | 51 | {obj.path} 52 | 53 | } 54 | 55 | 56 | {obj.targetRevision} 57 | 58 | 59 | ); 60 | }; 61 | 62 | export const useSourcesColumns = () => { 63 | 64 | const columns: TableColumn[] = React.useMemo( 65 | () => [ 66 | { 67 | title: 'Type', 68 | id: 'type', 69 | transforms: [], 70 | props: { className: 'gitops-admin-plugin__sources-type-column' } 71 | }, 72 | { 73 | title: 'Repository', 74 | id: 'repository', 75 | transforms: [sortable], 76 | sort: 'repository', 77 | }, 78 | { 79 | title: 'Chart', 80 | id: 'chart', 81 | transforms: [sortable], 82 | sort: 'chart', 83 | }, 84 | { 85 | title: 'Path', 86 | id: 'path', 87 | transforms: [sortable], 88 | sort: 'path', 89 | }, 90 | { 91 | title: 'Target Revision', 92 | sort: 'targetRevision', 93 | id: 'targetRevision', 94 | transforms: [sortable], 95 | } 96 | ], 97 | [], 98 | ); 99 | 100 | return columns; 101 | }; 102 | 103 | export default SourceList; 104 | -------------------------------------------------------------------------------- /src/externalsecrets/components/hooks/useESActionsProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | 4 | import { useModal } from '@utils/components/ModalProvider/ModalProvider'; 5 | import { Action, K8sVerb, k8sUpdate, useAnnotationsModal, useLabelsModal } from '@openshift-console/dynamic-plugin-sdk'; 6 | 7 | import ResourceDeleteModal from '@utils/components/ResourceDeleteModal/ResourceDeleteModal'; 8 | import { ExternalSecretKind, ExternalSecretModel, externalSecretModelRef } from '@es-models/ExternalSecrets'; 9 | 10 | type UseESActionsProvider = ( 11 | externalSecret: ExternalSecretKind, 12 | ) => [actions: Action[] /*, onOpen: () => void*/]; 13 | const t = (key: string) => key; 14 | 15 | export const useESActionsProvider: UseESActionsProvider = (externalSecret) => { 16 | const { createModal } = useModal(); 17 | const history = useHistory(); 18 | 19 | const launchLabelsModal = useLabelsModal(externalSecret); 20 | const launchAnnotationsModal = useAnnotationsModal(externalSecret); 21 | 22 | const actions = React.useMemo( 23 | () => [ 24 | { 25 | id: 'es-action-edit-labels', 26 | disabled: false, 27 | label: t('Edit labels'), 28 | accessReview: { 29 | group: ExternalSecretModel.apiGroup, 30 | verb: 'patch' as K8sVerb, 31 | resource: ExternalSecretModel.plural, 32 | namespace: externalSecret?.metadata?.namespace 33 | }, 34 | cta: () => {launchLabelsModal()} 35 | }, 36 | { 37 | id: 'es-action-edit-annotations', 38 | disabled: false, 39 | label: t('Edit annotations'), 40 | accessReview: { 41 | group: ExternalSecretModel.apiGroup, 42 | verb: 'patch' as K8sVerb, 43 | resource: ExternalSecretModel.plural, 44 | namespace: externalSecret?.metadata?.namespace 45 | }, 46 | cta: () => {launchAnnotationsModal()} 47 | }, 48 | { 49 | id: 'es-action-edit-crontab', 50 | disabled: false, 51 | label: t('Edit'), 52 | accessReview: { 53 | group: ExternalSecretModel.apiGroup, 54 | verb: 'update' as K8sVerb, 55 | resource: ExternalSecretModel.plural, 56 | namespace: externalSecret?.metadata?.namespace 57 | }, 58 | cta: () => 59 | history.push( 60 | `/k8s/ns/${externalSecret.metadata.namespace}/${externalSecretModelRef}/${externalSecret.metadata.name}/yaml`, 61 | ), 62 | }, 63 | { 64 | id: 'es-action-delete', 65 | label: t('Delete'), 66 | accessReview: { 67 | group: ExternalSecretModel.apiGroup, 68 | verb: 'delete' as K8sVerb, 69 | resource: ExternalSecretModel.plural, 70 | namespace: externalSecret?.metadata?.namespace 71 | }, 72 | cta: () => 73 | createModal(({ isOpen, onClose }) => ( 74 | 80 | )), 81 | }, 82 | { 83 | id: 'es-action-refresh', 84 | label: t('Refresh'), 85 | accessReview: { 86 | group: ExternalSecretModel.apiGroup, 87 | verb: 'update' as K8sVerb, 88 | resource: ExternalSecretModel.plural, 89 | namespace: externalSecret?.metadata?.namespace 90 | }, 91 | cta: () => { 92 | if (!externalSecret.metadata.annotations) { 93 | externalSecret.metadata.annotations = {}; 94 | } 95 | externalSecret.metadata.annotations["force-sync"] = "" + Math.round(new Date().getTime() / 1000); 96 | k8sUpdate({ 97 | model: ExternalSecretModel, 98 | data: externalSecret, 99 | }); 100 | } 101 | }, 102 | ], 103 | [/*t, */ externalSecret, createModal /*, dataSource*/, history], 104 | ); 105 | 106 | return [actions]; 107 | }; 108 | -------------------------------------------------------------------------------- /src/gitops/models/ApplicationModel.ts: -------------------------------------------------------------------------------- 1 | import { HealthStatus, PhaseStatus, SyncStatus } from 'src/gitops/utils/constants'; 2 | import { modelToRef } from 'src/gitops/utils/utils'; 3 | import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; 4 | import { K8sModel } from '@openshift-console/dynamic-plugin-sdk/lib/api/common-types'; 5 | 6 | export const ApplicationModel: K8sModel = { 7 | label: 'Application', 8 | labelPlural: 'Applications', 9 | apiVersion: 'v1alpha1', 10 | apiGroup: 'argoproj.io', 11 | plural: 'applications', 12 | abbr: 'app', 13 | namespaced: true, 14 | kind: 'Application', 15 | id: 'application', 16 | crd: true 17 | }; 18 | 19 | export type ApplicationSource = { 20 | chart: string, 21 | directory: { 22 | exclude?: string, 23 | include?: string, 24 | recurse?: boolean 25 | } 26 | helm: { 27 | releaseName?: string, 28 | version?: string 29 | } 30 | kustomize: { 31 | namePrefix?: string, 32 | nameSuffix?: string, 33 | version?: string 34 | } 35 | plugin?: { 36 | name: string 37 | } 38 | path?: string, 39 | ref?: string, 40 | repoURL?: string, 41 | targetRevision?: string 42 | } 43 | 44 | export type Retry = { 45 | limit?: number, 46 | backoff?: { 47 | duration?: string, 48 | factor?: number, 49 | maxDuration?: string 50 | } 51 | } 52 | 53 | export type SyncPolicy = { 54 | automated?: { 55 | selfHeal?: boolean, 56 | prune?: boolean, 57 | allowEmpty?: boolean, 58 | } 59 | retry?: Retry, 60 | syncOptions?: string[] 61 | } 62 | 63 | export type ApplicationSpec = { 64 | destination?: { 65 | namespace?: string, 66 | server?: string 67 | }, 68 | project?: string, 69 | source?: ApplicationSource, 70 | sources?: ApplicationSource[], 71 | syncPolicy?: SyncPolicy 72 | } 73 | 74 | export type ApplicationHistory = { 75 | deployStartedAt?: string, 76 | deployedAt?: string, 77 | id?: number, 78 | revision: string, 79 | source: ApplicationSource 80 | } 81 | 82 | export type Resource = { 83 | kind: string, 84 | group?: string, 85 | name: string, 86 | namespace?: string, 87 | } 88 | 89 | export type ApplicationResourceStatus = Resource & { 90 | hookPhase?: string, 91 | hookType?: string, 92 | message?: string; 93 | version?: string, 94 | syncWave?: number, 95 | status?: string 96 | health?: { 97 | status?: string, 98 | message?: string 99 | } 100 | } 101 | 102 | export type ApplicationCondition = { 103 | lastTransitionTime?: string, 104 | message?: string, 105 | type?: string 106 | } 107 | 108 | export type InitiatedBy = { 109 | username?: string, 110 | automated?: boolean 111 | } 112 | 113 | export type OperationState = { 114 | finishedAt?: string, 115 | message?: string, 116 | operation?: { 117 | initiatedBy?: InitiatedBy, 118 | retry?: { 119 | limit?: number 120 | } 121 | sync?: { 122 | revision: string 123 | } 124 | }, 125 | phase?: PhaseStatus, 126 | startedAt?: string, 127 | syncResult?: { 128 | resources?: ApplicationResourceStatus[] 129 | } 130 | } 131 | 132 | export type CurrentSyncStatus = { 133 | revision?: string, 134 | status?: SyncStatus 135 | } 136 | 137 | export type ApplicationStatus = { 138 | conditions?: ApplicationCondition[], 139 | // Added in Argo CD 2.8 140 | controllerNamespace?: string, 141 | sync?: CurrentSyncStatus, 142 | health?: { 143 | status?: HealthStatus 144 | } 145 | history?: ApplicationHistory[], 146 | operationState?: OperationState, 147 | reconciledAt?: string 148 | resources?: ApplicationResourceStatus[]; 149 | sourceType?: string 150 | } 151 | 152 | export type Info = { 153 | name: string, 154 | value: string 155 | } 156 | 157 | export type SyncOperation = { 158 | dryRun?: boolean, 159 | manifests?: string[], 160 | prune?: boolean, 161 | resources?: Resource[] 162 | revisions?: string[], 163 | source?: ApplicationSource, 164 | sources?: ApplicationSource[], 165 | syncOptions?: string[], 166 | } 167 | 168 | export type ApplicationOperation = { 169 | info?: Info[], 170 | initiatedBy?: InitiatedBy, 171 | retry?: Retry, 172 | sync?: SyncOperation 173 | } 174 | 175 | export type ApplicationKind = K8sResourceCommon & { 176 | spec?: ApplicationSpec, 177 | operation? : ApplicationOperation, 178 | status?: ApplicationStatus 179 | }; 180 | 181 | export const applicationModelRef = modelToRef(ApplicationModel); 182 | -------------------------------------------------------------------------------- /src/externalsecrets/components/ESDetailsTab.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { RouteComponentProps } from 'react-router'; 3 | import { 4 | DescriptionList, 5 | Grid, 6 | GridItem, 7 | PageSection, 8 | PageSectionVariants, 9 | Title 10 | } from '@patternfly/react-core'; 11 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 12 | import { ExternalSecretKind, ExternalSecretModel } from '@es-models/ExternalSecrets'; 13 | import { getObjectModifyPermissions } from '@gitops-utils/utils'; 14 | import StandardDetailsGroup from '@utils/components/StandardDetailsGroup/StandardDetailsGroup'; 15 | import { DetailsDescriptionGroup } from '@utils/components/DetailsDescriptionGroup/DetailsDescriptionGroup'; 16 | import ESStatus from './ESStatus'; 17 | import { Conditions } from '@utils/components/Conditions/conditions'; 18 | import { K8sGroupVersionKind, ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; 19 | import { getTargetSecretName } from '../utils/es-utils'; 20 | 21 | type ESDetailsTabProps = RouteComponentProps<{ 22 | ns: string; 23 | name: string; 24 | }> & { 25 | obj?: ExternalSecretKind; 26 | }; 27 | 28 | const ESDetailsTab: React.FC = ({ obj }) => { 29 | const { t } = useGitOpsTranslation(); 30 | 31 | const gvk: K8sGroupVersionKind = { 32 | version: "v1beta1", 33 | group: "external-secrets.io", 34 | kind: obj.spec.secretStoreRef.kind ? obj.spec.secretStoreRef.kind : "SecretStore" 35 | } 36 | 37 | const [canPatch] = getObjectModifyPermissions(obj, ExternalSecretModel); 38 | 39 | return ( 40 |
41 | 42 | 43 | {t('ExternalSecret details')} 44 | 45 | 46 | 47 | 48 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 69 | 70 | 71 | 72 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | {t('Conditions')} 86 | 87 | {obj.status?.conditions ? 88 | 89 | : 90 | <>No Conditions 91 | } 92 | 93 |
94 | ); 95 | }; 96 | 97 | export default ESDetailsTab; 98 | -------------------------------------------------------------------------------- /src/gitops/components/project/ProjectWindowsTab.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { RowProps, TableColumn, TableData, VirtualizedTable } from '@openshift-console/dynamic-plugin-sdk'; 4 | import { RouteComponentProps } from 'react-router'; 5 | import { sortable } from '@patternfly/react-table'; 6 | import { List, ListItem, PageSection, PageSectionVariants } from '@patternfly/react-core'; 7 | import { AppProjectKind, SyncWindow } from '@gitops-models/AppProjectModel'; 8 | 9 | type AppProjectWindowsProps = RouteComponentProps<{ 10 | ns: string; 11 | name: string; 12 | }> & { 13 | obj?: AppProjectKind; 14 | }; 15 | 16 | const ProjectWindowsTab: React.FC = ({ obj }) => { 17 | 18 | //const { t } = useGitOpsTranslation(); 19 | 20 | var windows: SyncWindow[]; 21 | if (obj?.spec?.syncWindows) { 22 | windows = obj.spec.syncWindows; 23 | } else { 24 | windows = []; 25 | } 26 | 27 | return ( 28 |
29 | 30 | 38 | 39 |
40 | ) 41 | } 42 | 43 | const windowsListRow: React.FC> = ({ obj, activeColumnIDs }) => { 44 | 45 | return ( 46 | <> 47 | 48 | {obj.kind} 49 | 50 | 51 | {obj.schedule} 52 | 53 | 54 | {obj.duration} 55 | 56 | 57 | {obj.applications && 58 | 59 | {obj.applications.map(el=> {el})} 60 | 61 | } 62 | 63 | 64 | {obj.applications && 65 | 66 | {obj.namespaces.map(el=> {el})} 67 | 68 | } 69 | 70 | 71 | {obj.applications && 72 | 73 | {obj.clusters.map(el=> {el})} 74 | 75 | } 76 | 77 | 78 | {obj.manualSync?obj.manualSync:"False"} 79 | 80 | 81 | ); 82 | }; 83 | 84 | export const useWindowsColumns = () => { 85 | 86 | const columns: TableColumn[] = React.useMemo( 87 | () => [ 88 | { 89 | title: 'Kind', 90 | id: 'kind', 91 | transforms: [sortable], 92 | sort: `kind` 93 | }, 94 | { 95 | title: 'Schedule', 96 | id: 'schedule', 97 | transforms: [sortable], 98 | sort: `schedule` 99 | }, 100 | { 101 | title: 'Duration', 102 | id: 'duration', 103 | transforms: [sortable], 104 | sort: `duration` 105 | }, 106 | { 107 | title: 'Applications', 108 | id: 'applications' 109 | }, 110 | { 111 | title: 'Namespaces', 112 | id: 'namespaces' 113 | }, 114 | { 115 | title: 'Clusters', 116 | id: 'clusters' 117 | }, 118 | { 119 | title: 'Manual Sync', 120 | id: 'manualSync', 121 | transforms: [sortable], 122 | sort: `manualSync` 123 | } 124 | ], 125 | [], 126 | ); 127 | 128 | return columns; 129 | }; 130 | 131 | export default ProjectWindowsTab; 132 | -------------------------------------------------------------------------------- /src/utils/components/StandardDetailsGroup/StandardDetailsGroup.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useGitOpsTranslation } from "@utils/hooks/useGitOpsTranslation"; 3 | import PencilAltIcon from '@patternfly/react-icons/dist/esm/icons/pencil-alt-icon'; 4 | import { K8sModel, K8sResourceCommon, ResourceLink, Timestamp, useAnnotationsModal, useLabelsModal } from "@openshift-console/dynamic-plugin-sdk"; 5 | import { Button, DescriptionListDescription, DescriptionListGroup, DescriptionListTermHelpText, DescriptionListTermHelpTextButton, Popover, Split, SplitItem } from "@patternfly/react-core"; 6 | import MetadataLabels from './MetadataLabels'; 7 | import { DetailsDescriptionGroup } from '../DetailsDescriptionGroup/DetailsDescriptionGroup'; 8 | 9 | export enum Details { 10 | Name, 11 | Namespace, 12 | Labels, 13 | Annotations, 14 | Created 15 | } 16 | 17 | type StandardDetailsGroupProps = { 18 | obj: K8sResourceCommon; 19 | model: K8sModel; 20 | canPatch: [boolean, boolean]; 21 | exclude: Details[]; 22 | }; 23 | 24 | const StandardDetailsGroup: React.FC = ({ obj, model, canPatch, exclude }) => { 25 | const { t } = useGitOpsTranslation(); 26 | 27 | const launchLabelsModal = useLabelsModal(obj); 28 | const launchAnnotationsModal = useAnnotationsModal(obj); 29 | 30 | return ( 31 | <> 32 | {!(Details.Name in exclude) && 33 | 34 | 35 | {obj?.metadata?.name} 36 | 37 | } 38 | 39 | {!(Details.Namespace in exclude) && 40 | 41 | 42 | 43 | 44 | } 45 | 46 | {!(Details.Labels in exclude) && 47 | 48 | 49 | 50 | 51 | {t('Labels')}} bodyContent={
{t('Map of string keys and values that can be used to organize and categorize (scope and select) objects.')}
}> 52 | 53 | {t('Labels')} 54 | 55 |
56 |
57 |
58 | 59 | 60 | 61 |
62 | 63 | 64 | 65 |
66 | } 67 | 68 | {!(Details.Annotations in exclude) && 69 | 70 | 71 | {t('Annotations')}} bodyContent={
{t('Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects.')}
}> 72 | 73 | {t('Annotations')} 74 | 75 |
76 |
77 | 78 |
79 | 80 |
81 |
82 |
83 | } 84 | 85 | {!(Details.Created in exclude) && 86 | 87 | 88 | 89 | } 90 | 91 | ) 92 | } 93 | 94 | export default StandardDetailsGroup; 95 | -------------------------------------------------------------------------------- /src/rollout/components/RolloutDetailsTab.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { RouteComponentProps } from 'react-router'; 3 | import { 4 | DescriptionList, 5 | Grid, 6 | GridItem, 7 | NumberInput, 8 | PageSection, 9 | PageSectionVariants, 10 | Title 11 | } from '@patternfly/react-core'; 12 | import { useGitOpsTranslation } from '@utils/hooks/useGitOpsTranslation'; 13 | import { getObjectModifyPermissions } from '@gitops-utils/utils'; 14 | import StandardDetailsGroup from '@utils/components/StandardDetailsGroup/StandardDetailsGroup'; 15 | import { RolloutModel } from '@rollout-models/RolloutModel'; 16 | import BlueGreenServices from './Strategy/BlueGreenServices'; 17 | import CanaryServices from './Strategy/CanaryServices'; 18 | import { k8sUpdate } from '@openshift-console/dynamic-plugin-sdk'; 19 | import { DetailsDescriptionGroup } from '@utils/components/DetailsDescriptionGroup/DetailsDescriptionGroup'; 20 | import { RolloutStatusFragment } from './RolloutStatus'; 21 | import { Conditions } from '@utils/components/Conditions/conditions'; 22 | 23 | type RolloutDetailsTabProps = RouteComponentProps<{ 24 | ns: string; 25 | name: string; 26 | }> & { 27 | obj?: any; 28 | }; 29 | 30 | const RolloutDetailsTab: React.FC = ({ obj }) => { 31 | const { t } = useGitOpsTranslation(); 32 | 33 | const [canPatch, canUpdate] = getObjectModifyPermissions(obj, RolloutModel); 34 | 35 | const onReplicaChange = (event: React.FormEvent) => { 36 | const value = (event.target as HTMLInputElement).value; 37 | if (obj.spec.replicas == value) return; 38 | obj.spec.replicas = value; 39 | k8sUpdate({ 40 | model: RolloutModel, 41 | data: obj 42 | }); 43 | }; 44 | 45 | const onReplicaPlus = () => { 46 | obj.spec.replicas = (obj.spec.replicas || 0) + 1; 47 | k8sUpdate({ 48 | model: RolloutModel, 49 | data: obj 50 | }); 51 | }; 52 | 53 | const onReplicaMinus = () => { 54 | obj.spec.replicas = (obj.spec.replicas || 0) - 1; 55 | k8sUpdate({ 56 | model: RolloutModel, 57 | data: obj 58 | }); 59 | }; 60 | 61 | return ( 62 |
63 | 64 | 65 | {t('Rollout details')} 66 | 67 | 68 | 69 | 70 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | {obj?.spec?.strategy?.blueGreen ? "Blue-Green" : "Canary"} 102 | 103 | 104 | {obj?.spec?.strategy?.blueGreen ? 105 | 106 | : 107 | 108 | } 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | {t('Conditions')} 117 | 118 | {obj.status?.conditions ? 119 | 120 | : 121 | <>No Conditions 122 | } 123 | 124 |
125 | ); 126 | }; 127 | 128 | export default RolloutDetailsTab; 129 | --------------------------------------------------------------------------------