├── .gitignore ├── README.md ├── images └── applicationset-extension.png ├── manifests ├── apps-of-appset.yaml ├── extension.yaml └── resources │ └── applicationset-guestbook.yaml └── ui ├── dist └── extension.tar ├── package.json ├── src ├── index.tsx ├── model │ ├── applicationset.ts │ ├── commons.ts │ └── tree.ts └── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | ui/node_modules 2 | ui/dist/resources 3 | ui/package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Application set extension 2 | 3 | An extension to use with Applicationset in order to easily identify the number applications and their status 4 | 5 | ![extension](./images/applicationset-extension.png) 6 | 7 | ## Install 8 | 9 | You need to setup the [ArgoCD Extension Controller](https://github.com/argoproj-labs/argocd-extensions) 10 | 11 | Then to install: 12 | 13 | - `kubectl apply -f manifests/extensions.yaml` 14 | - Install the apps of applicationset `kubectl apply -f manifests/apps-of-appset.yaml` -------------------------------------------------------------------------------- /images/applicationset-extension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/speedfl/argocd-applicationset-extension/6db6c248aac6f04aaa70f73a2195133e54e961d6/images/applicationset-extension.png -------------------------------------------------------------------------------- /manifests/apps-of-appset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: apps-of-appset 5 | namespace: argocd 6 | finalizers: 7 | - resources-finalizer.argocd.argoproj.io 8 | spec: 9 | destination: 10 | namespace: argocd 11 | server: https://kubernetes.default.svc 12 | project: default 13 | source: 14 | path: manifests/resources 15 | repoURL: https://github.com/speedfl/argocd-apps-of-applicationset.git 16 | targetRevision: master 17 | syncPolicy: 18 | automated: 19 | prune: true 20 | allowEmpty: true 21 | selfHeal: true 22 | -------------------------------------------------------------------------------- /manifests/extension.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: ArgoCDExtension 3 | metadata: 4 | name: argocd-appset-ext 5 | labels: 6 | tab: "ApplicationSet" 7 | icon: "fa-files" 8 | finalizers: 9 | - extensions-finalizer.argocd.argoproj.io 10 | spec: 11 | sources: 12 | - web: 13 | url: https://raw.githubusercontent.com/speedfl/argocd-apps-of-applicationset/master/ui/dist/extension.tar -------------------------------------------------------------------------------- /manifests/resources/applicationset-guestbook.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: ApplicationSet 3 | metadata: 4 | name: guestbook 5 | spec: 6 | goTemplate: true 7 | generators: 8 | - list: 9 | elements: 10 | - name: guestbook-one 11 | url: https://kubernetes.default.svc 12 | template: 13 | metadata: 14 | name: '{{.name}}' 15 | spec: 16 | project: default 17 | source: 18 | repoURL: https://github.com/argoproj/argocd-example-apps.git 19 | targetRevision: master 20 | path: guestbook 21 | destination: 22 | server: '{{.url}}' 23 | namespace: gmuselli 24 | syncPolicy: 25 | automated: 26 | prune: true 27 | allowEmpty: true 28 | selfHeal: true -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application-set", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "peerDeependencies": { 7 | "react": "^16.9.3" 8 | }, 9 | "scripts": { 10 | "build": "webpack --config ./webpack.config.js && tar -C dist -cvf dist/extension.tar resources" 11 | }, 12 | "devDependencies": { 13 | "@types/react": "^16.8.5", 14 | "css-loader": "^6.7.3", 15 | "mini-css-extract-plugin": "^2.7.2", 16 | "node-sass": "^8.0.0", 17 | "raw-loader": "^4.0.2", 18 | "react": "^16.9.3", 19 | "react-moment": "^0.9.7", 20 | "sass": "^1.34.1", 21 | "sass-loader": "^13.2.0", 22 | "style-loader": "^3.3.1", 23 | "ts-loader": "9.4.2" 24 | }, 25 | "dependencies": { 26 | "typescript": "^4.9.3", 27 | "webpack": "^5.75.0", 28 | "webpack-cli": "^5.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Moment from "react-moment"; 3 | import { ApplicationSet } from "./model/applicationset"; 4 | import { HealthStatus, Tree } from "./model/tree"; 5 | 6 | const MAP_STATUS = { 7 | Healthy: { name: "fa-heart", spin: false, color: "rgb(24, 190, 148)" }, 8 | Progressing: { 9 | name: "fa-circle-notch", 10 | spin: true, 11 | color: "rgb(13, 173, 234)", 12 | }, 13 | Degraded: { 14 | name: "fa-heart-broken", 15 | spin: false, 16 | color: "rgb(233, 109, 118)", 17 | }, 18 | Suspended: { 19 | name: "fa-pause-circle", 20 | spin: false, 21 | color: "rgb(118, 111, 148)", 22 | }, 23 | Missing: { name: "fa-ghost", spin: false, color: "rgb(244, 192, 48)" }, 24 | Unknown: { 25 | name: "fa-question-circle", 26 | spin: false, 27 | color: "rgb(204, 214, 221)", 28 | }, 29 | }; 30 | 31 | export const Extension = (props: { tree: Tree; resource: ApplicationSet }) => { 32 | console.log(props); 33 | 34 | var items = props.tree.nodes.filter((item) => 35 | // filter the one owned by the ApplicationSet 36 | item.parentRefs?.find( 37 | (parentRef) => parentRef.uid === props.resource.metadata.uid 38 | ) 39 | ); 40 | 41 | return ( 42 |
43 |
52 |
58 | {Object.keys(MAP_STATUS).map((key: HealthStatus) => ( 59 |
67 | 76 | {key}: {items.filter((item) => item.health.status == key).length} 77 |
78 | ))} 79 |
80 |
81 | 82 |
83 | {items.map((item) => ( 84 |
97 |
105 | 106 |
107 |
108 | application 109 |
110 |
111 |
120 |
129 | {item.name} 130 |
131 |
132 | 140 | 141 | 142 | 143 |
144 |
145 |
146 | {item.createdAt ? ( 147 | 161 | {item.createdAt} 162 | 163 | ) : null} 164 |
165 |
166 | ))} 167 |
168 |
169 | ); 170 | }; 171 | 172 | export const component = Extension; 173 | -------------------------------------------------------------------------------- /ui/src/model/applicationset.ts: -------------------------------------------------------------------------------- 1 | import * as models from "./commons" 2 | 3 | interface ItemsList { 4 | /** 5 | * APIVersion defines the versioned schema of this representation of an object. 6 | * Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. 7 | */ 8 | apiVersion?: string; 9 | items: T[]; 10 | /** 11 | * Kind is a string value representing the REST resource this object represents. 12 | * Servers may infer this from the endpoint the client submits requests to. 13 | */ 14 | kind?: string; 15 | metadata: models.ListMeta; 16 | } 17 | 18 | export interface ApplicationSetList extends ItemsList {} 19 | 20 | export interface SecretRef { 21 | secretName: string; 22 | key: string; 23 | } 24 | 25 | export interface ApplicationSet { 26 | apiVersion?: string; 27 | kind?: string; 28 | metadata: models.ObjectMeta; 29 | spec: ApplicationSetSpec; 30 | status?: ApplicationSetStatus; 31 | } 32 | 33 | export interface ApplicationSetSpec { 34 | goTemplate?: boolean; 35 | generators: ApplicationSetGenerator[]; 36 | template: ApplicationSetTemplate; 37 | syncPolicy?: ApplicationSetSyncPolicy; 38 | } 39 | 40 | export interface ApplicationSetSyncPolicy { 41 | preserveResourcesOnDeletion?: boolean; 42 | } 43 | 44 | export interface ApplicationSetTemplate { 45 | spec: { [key: string]: any}; 46 | } 47 | 48 | export interface ApplicationSetTemplateMeta { 49 | name?: string; 50 | namespace?: string; 51 | labels?: { [key: string]: string}; 52 | annotations?: { [key: string]: string}; 53 | finalizers?: string[]; 54 | } 55 | 56 | export interface ApplicationSetGenerator { 57 | list?: ListGenerator; 58 | clusters?: ClusterGenerator; 59 | git?: GitGenerator; 60 | scmProvider?: SCMProviderGenerator; 61 | clusterDecisionResource?: DuckTypeGenerator; 62 | pullRequest?: PullRequestGenerator; 63 | matrix?: MatrixGenerator; 64 | merge?: MergeGenerator; 65 | selector?: { [key: string]: string}; 66 | } 67 | 68 | export interface ApplicationSetNestedGenerator { 69 | list?: ListGenerator; 70 | clusters?: ClusterGenerator; 71 | git?: GitGenerator; 72 | scmProvider?: SCMProviderGenerator; 73 | clusterDecisionResource?: DuckTypeGenerator; 74 | pullRequest?: PullRequestGenerator; 75 | matrix?: { [key: string]: string}; 76 | merge?: { [key: string]: string}; 77 | selector?: { [key: string]: string}; 78 | } 79 | 80 | export interface ApplicationSetTerminalGenerator { 81 | list?: ListGenerator; 82 | clusters?: ClusterGenerator; 83 | git?: GitGenerator; 84 | scmProvider?: SCMProviderGenerator; 85 | clusterDecisionResource?: DuckTypeGenerator; 86 | pullRequest?: PullRequestGenerator; 87 | } 88 | 89 | export interface ListGenerator { 90 | elements: { [key: string]: string}[]; 91 | template?: ApplicationSetTemplate; 92 | } 93 | 94 | export interface MatrixGenerator { 95 | generators: ApplicationSetNestedGenerator[]; 96 | template?: ApplicationSetTemplate; 97 | } 98 | 99 | export interface NestedMatrixGenerator { 100 | generators: ApplicationSetTerminalGenerator[]; 101 | } 102 | 103 | export interface MergeGenerator { 104 | generators: ApplicationSetNestedGenerator[]; 105 | mergeKeys: string[]; 106 | template?: ApplicationSetTemplate; 107 | } 108 | 109 | export interface NestedMergeGenerator { 110 | generators: ApplicationSetTerminalGenerator[]; 111 | mergeKeys: string[]; 112 | } 113 | 114 | export interface ClusterGenerator { 115 | selector?: { [key: string]: string}; 116 | template?: ApplicationSetTemplate; 117 | values?: { [key: string]: string}; 118 | } 119 | 120 | export interface DuckTypeGenerator { 121 | configMapRef: string; 122 | name?: string; 123 | requeueAfterSeconds?: number; 124 | labelSelector?: { [key: string]: string}; 125 | template?: ApplicationSetTemplate; 126 | values?: { [key: string]: string}; 127 | } 128 | 129 | export interface GitGenerator { 130 | repoURL: string; 131 | directories?: GitDirectoryGeneratorItem[]; 132 | files?: GitFileGeneratorItem[]; 133 | revision: string; 134 | requeueAfterSeconds?: number; 135 | template?: ApplicationSetTemplate; 136 | } 137 | 138 | export interface GitDirectoryGeneratorItem { 139 | path: string; 140 | exclude?: boolean; 141 | } 142 | 143 | export interface GitFileGeneratorItem { 144 | path: string; 145 | } 146 | 147 | export interface SCMProviderGenerator { 148 | github?: SCMProviderGeneratorGithub; 149 | gitlab?: SCMProviderGeneratorGitlab; 150 | bitbucket?: SCMProviderGeneratorBitbucket; 151 | bitbucketServer?: SCMProviderGeneratorBitbucketServer; 152 | gitea?: SCMProviderGeneratorGitea; 153 | azureDevOps?: SCMProviderGeneratorAzureDevOps; 154 | filters?: SCMProviderGeneratorFilter[]; 155 | cloneProtocol?: string; 156 | requeueAfterSeconds?: number; 157 | template?: ApplicationSetTemplate; 158 | } 159 | 160 | export interface SCMProviderGeneratorGitea { 161 | owner: string; 162 | api: string; 163 | tokenRef?: SecretRef; 164 | allBranches?: boolean; 165 | insecure?: boolean; 166 | } 167 | 168 | export interface SCMProviderGeneratorGithub { 169 | organization: string; 170 | api?: string; 171 | tokenRef?: SecretRef; 172 | appSecretName?: string; 173 | allBranches?: boolean; 174 | } 175 | 176 | export interface SCMProviderGeneratorGitlab { 177 | group: string; 178 | includeSubgroups?: boolean; 179 | api?: string; 180 | tokenRef?: SecretRef; 181 | allBranches?: boolean; 182 | } 183 | 184 | export interface SCMProviderGeneratorBitbucket { 185 | owner: string; 186 | user: string; 187 | appPasswordRef?: SecretRef; 188 | allBranches?: boolean; 189 | } 190 | 191 | export interface SCMProviderGeneratorBitbucketServer { 192 | project: string; 193 | api: string; 194 | basicAuth?: BasicAuthBitbucketServer; 195 | allBranches?: boolean; 196 | } 197 | 198 | export interface SCMProviderGeneratorAzureDevOps { 199 | organization: string; 200 | api?: string; 201 | teamProject: string; 202 | accessTokenRef?: SecretRef; 203 | allBranches?: boolean; 204 | } 205 | 206 | export interface SCMProviderGeneratorFilter { 207 | repositoryMatch?: string; 208 | pathsExist?: string[]; 209 | pathsDoNotExist?: string[]; 210 | labelMatch?: string; 211 | branchMatch?: string; 212 | } 213 | 214 | export interface PullRequestGenerator { 215 | github?: PullRequestGeneratorGithub; 216 | gitlab?: PullRequestGeneratorGitLab; 217 | gitea?: PullRequestGeneratorGitea; 218 | bitbucketServer?: PullRequestGeneratorBitbucketServer; 219 | filters?: PullRequestGeneratorFilter[]; 220 | requeueAfterSeconds?: number; 221 | template?: ApplicationSetTemplate; 222 | } 223 | 224 | export interface PullRequestGeneratorGitea { 225 | owner: string; 226 | repo: string; 227 | api: string; 228 | tokenRef?: SecretRef; 229 | insecure?: boolean; 230 | } 231 | 232 | export interface PullRequestGeneratorGithub { 233 | owner: string; 234 | repo: string; 235 | api?: string; 236 | tokenRef?: SecretRef; 237 | appSecretName?: string; 238 | labels?: string[]; 239 | } 240 | 241 | export interface PullRequestGeneratorGitLab { 242 | project: string; 243 | api?: string; 244 | tokenRef?: SecretRef; 245 | labels?: string[]; 246 | pullRequestState?: string; 247 | } 248 | 249 | export interface PullRequestGeneratorBitbucketServer { 250 | project: string; 251 | repo: string; 252 | api: string; 253 | basicAuth?: BasicAuthBitbucketServer; 254 | } 255 | 256 | export interface BasicAuthBitbucketServer { 257 | username: string; 258 | passwordRef?: SecretRef; 259 | } 260 | 261 | export interface PullRequestGeneratorFilter { 262 | branchMatch?: string; 263 | } 264 | 265 | export interface ApplicationSetStatus { 266 | conditions?: ApplicationSetCondition[]; 267 | } 268 | 269 | export type ApplicationSetConditionType = 'ErrorOccurred' | 'ParametersGenerated' | 'ResourcesUpToDate'; 270 | 271 | export type ApplicationSetConditionStatus = 'True' | 'False' | 'Unknown' 272 | 273 | export interface ApplicationSetCondition { 274 | type: ApplicationSetConditionType; 275 | message: string; 276 | lastTransitionTime?: string; 277 | status: ApplicationSetConditionStatus; 278 | reason: string; 279 | } -------------------------------------------------------------------------------- /ui/src/model/commons.ts: -------------------------------------------------------------------------------- 1 | export type Time = string; 2 | 3 | export interface ListMeta { 4 | continue?: string; 5 | resourceVersion?: string; 6 | selfLink?: string; 7 | } 8 | 9 | export interface ObjectMeta { 10 | name?: string; 11 | generateName?: string; 12 | namespace?: string; 13 | selfLink?: string; 14 | uid?: string; 15 | resourceVersion?: string; 16 | generation?: number; 17 | creationTimestamp?: Time; 18 | deletionTimestamp?: Time; 19 | deletionGracePeriodSeconds?: number; 20 | labels?: {[name: string]: string}; 21 | annotations?: {[name: string]: string}; 22 | ownerReferences?: any[]; 23 | initializers?: any; 24 | finalizers?: string[]; 25 | clusterName?: string; 26 | } 27 | 28 | export interface TypeMeta { 29 | kind?: string; 30 | apiVersion?: string; 31 | } -------------------------------------------------------------------------------- /ui/src/model/tree.ts: -------------------------------------------------------------------------------- 1 | export type HealthStatus = 'Healthy' | 'Degraded' | 'Progressing' | 'Unknown' | 'Suspended' | 'Missing'; 2 | 3 | export interface Health { 4 | status: HealthStatus; 5 | } 6 | 7 | export interface NodeBase { 8 | group: string; 9 | kind: string; 10 | namespace: string; 11 | name: string; 12 | uid: string; 13 | } 14 | 15 | export interface Node extends NodeBase { 16 | version: string; 17 | parentRefs?: Node[]; 18 | resourceVersion: string; 19 | health: Health; 20 | createdAt: string; 21 | } 22 | 23 | export interface Tree { 24 | nodes: Node[]; 25 | } -------------------------------------------------------------------------------- /ui/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "sourceMap": true, 5 | "noImplicitAny": true, 6 | "module": "esnext", 7 | "target": "es5", 8 | "jsx": "react", 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "noUnusedLocals": true, 12 | "declaration": false, 13 | "allowSyntheticDefaultImports": true, 14 | "lib": ["es2017", "dom"] 15 | }, 16 | "include": ["./**/*"], 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const groupKind = 'argoproj.io/ApplicationSet'; 4 | 5 | const config = { 6 | entry: { 7 | extension: './src/index.tsx', 8 | }, 9 | output: { 10 | filename: 'extensions.js', 11 | path: __dirname + `/dist/resources/${groupKind}/ui`, 12 | libraryTarget: 'window', 13 | library: ['extensions', 'resources', groupKind], 14 | }, 15 | resolve: { 16 | extensions: ['.ts', '.tsx', '.js', '.json', '.ttf', '.scss'], 17 | }, 18 | externals: { 19 | react: 'React', 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.tsx?$/, 25 | loader: 'ts-loader', 26 | options: { 27 | allowTsInNodeModules: true, 28 | configFile: path.resolve('./src/tsconfig.json') 29 | }, 30 | }, 31 | { 32 | test: /\.scss$/, 33 | use: ['style-loader', 'raw-loader', 'sass-loader'], 34 | }, 35 | { 36 | test: /\.css$/, 37 | use: ['style-loader', 'raw-loader'], 38 | }, 39 | ], 40 | }, 41 | }; 42 | 43 | module.exports = config; --------------------------------------------------------------------------------