├── .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 | 
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 |
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;
--------------------------------------------------------------------------------