diff --git a/assets/schema.yml b/assets/schema.yml index cc825db..ea73f63 100644 --- a/assets/schema.yml +++ b/assets/schema.yml @@ -1,3 +1,10 @@ +- name: Resource + version: '1' + fields: + - { name: path, type: string, isRequired: true} + - { name: content, type: string, isRequired: true} + - { name: sha256sum, type: string, isRequired: true} + - name: VaultSecret version: '1' fields: @@ -16,12 +23,6 @@ - { name: managedTeams, type: string, isList: true, isRequired: true } - { name: automationToken, type: VaultSecret } -- name: ClusterManagedRole - version: '1' - fields: - - { name: namespace, type: string, isRequired: true } - - { name: role, type: string, isRequired: true } - - name: Cluster version: '1' fields: @@ -32,7 +33,37 @@ - { name: description, type: string, isRequired: true } - { name: serverUrl, type: string, isRequired: true } - { name: automationToken, type: VaultSecret } - - { name: managedRoles, type: ClusterManagedRole, isList: true } + +- name: NamespaceOpenshiftResource + version: '1' + isInterface: true + interfaceResolve: + strategy: fieldMap + field: provider + fieldMap: + resource: NamespaceOpenshiftResourceResource + fields: + - { name: provider, type: string, isRequired: true } + +- name: NamespaceOpenshiftResourceResource + version: '1' + interface: NamespaceOpenshiftResource + fields: + - { name: provider, type: string, isRequired: true } + - { name: path, type: string, isRequired: true } + +- name: Namespace + version: '1' + fields: + - { name: schema, type: string, isRequired: true } + - { name: path, type: string, isRequired: true } + - { name: labels, type: json } + - { name: name, type: string, isRequired: true } + - { name: description, type: string, isRequired: true } + - { name: cluster, type: Cluster, isRequired: true } + - { name: managedRoles, type: string, isList: true} + - { name: managedResourceTypes, type: string, isList: true} + - { name: openshiftResources, type: NamespaceOpenshiftResource, isList: true, isInterface: true} - name: AppServiceOwner version: '1' @@ -180,9 +211,11 @@ - name: Query fields: - - { name: user, type: User, isList: true, datafileSchema: /access/user-1.yml } - - { name: bot, type: Bot, isList: true, datafileSchema: /access/bot-1.yml } - - { name: role, type: Role, isList: true, datafileSchema: /access/role-1.yml } - - { name: cluster, type: Cluster, isList: true, datafileSchema: /openshift/cluster-1.yml } - - { name: quay_org, type: QuayOrg, isList: true, datafileSchema: /dependencies/quay-org-1.yml } - - { name: app, type: App, isList: true, datafileSchema: /app-sre/app-1.yml } + - { name: users, type: User, isList: true, datafileSchema: /access/user-1.yml } + - { name: bots, type: Bot, isList: true, datafileSchema: /access/bot-1.yml } + - { name: roles, type: Role, isList: true, datafileSchema: /access/role-1.yml } + - { name: clusters, type: Cluster, isList: true, datafileSchema: /openshift/cluster-1.yml } + - { name: namespaces, type: Namespace, isList: true, datafileSchema: /openshift/namespace-1.yml } + - { name: quay_orgs, type: QuayOrg, isList: true, datafileSchema: /dependencies/quay-org-1.yml } + - { name: apps, type: App, isList: true, datafileSchema: /app-sre/app-1.yml } + - { name: resources, type: Resource, isResource: true, isRequired: true, isList: true } diff --git a/assets/schemas/openshift/cluster-1.yml b/assets/schemas/openshift/cluster-1.yml index 1d68f92..64d2bee 100644 --- a/assets/schemas/openshift/cluster-1.yml +++ b/assets/schemas/openshift/cluster-1.yml @@ -20,19 +20,6 @@ properties: "$ref": "/common-1.json#/definitions/vaultSecret" description: type: string - managedRoles: - type: array - items: - type: object - additionalProperties: false - properties: - namespace: - type: string - role: - type: string - required: - - namespace - - role required: - "$schema" - labels diff --git a/assets/schemas/openshift/namespace-1.yml b/assets/schemas/openshift/namespace-1.yml new file mode 100644 index 0000000..9201856 --- /dev/null +++ b/assets/schemas/openshift/namespace-1.yml @@ -0,0 +1,71 @@ +--- +"$schema": /metaschema-1.json +version: '1.0' +type: object + +additionalProperties: false +properties: + "$schema": + type: string + enum: + - /openshift/namespace-1.yml + + labels: + "$ref": "/common-1.json#/definitions/labels" + + name: + type: string + + description: + type: string + + cluster: + "$ref": "/common-1.json#/definitions/crossref" + "$schemaRef": "/openshift/cluster-1.yml" + + managedRoles: + type: array + items: + type: string + enum: + - view + - edit + - admin + + managedResourceTypes: + type: array + items: + type: string + # For the moment we want to limit this list. + # A complete list can be obtained from here: + # oc api-resources --verbs=list --no-headers | awk '{print $NF}' | sort -u + enum: + - ConfigMap + + openshiftResources: + type: array + items: + type: object + properties: + provider: + type: string + oneOf: + - additionalProperties: false + properties: + provider: + type: string + enum: + - resource + path: + type: string + required: + - path + required: + - provider + +required: +- "$schema" +- labels +- name +- description +- cluster diff --git a/src/db.ts b/src/db.ts index 986ffca..b1cbc05 100644 --- a/src/db.ts +++ b/src/db.ts @@ -11,12 +11,23 @@ interface IDatafile { path: string; } +interface IResource { + path: string; + content: string; + sha256sum: string; +} + interface IDatafilesDict { [key: string]: any; } +interface IResourcesDict { + [key: string]: any; +} + // module variables let datafiles: IDatafilesDict = new Map(); +let resources: IResourcesDict = new Map(); let sha256sum: string = ''; // utils @@ -27,11 +38,6 @@ const getRefExpr = (ref: string): string => { return m ? m[0] : ''; }; -// filters -export function getDatafilesBySchema(schema: string): IDatafile[] { - return Object.values(datafiles).filter((d: any) => d.$schema === schema); -} - export function resolveRef(itemRef: any) { const path = getRefPath(itemRef.$ref); const expr = getRefExpr(itemRef.$ref); @@ -51,6 +57,20 @@ export function resolveRef(itemRef: any) { return resolvedData; } +// filters +export function getDatafilesBySchema(schema: string): IDatafile[] { + return Object.values(datafiles).filter((d: any) => d.$schema === schema); +} + +export function getResource(path: string): IResource { + return resources[path]; +} + +export function getResources(): IResource[] { + return Object.values(resources); +} + +// loader function validateDatafile(d: any) { const datafilePath: any = d[0]; const datafileData: any = d[1]; @@ -64,17 +84,33 @@ function validateDatafile(d: any) { !('$schema' in datafileData)) { throw new Error('Invalid datafileData object'); } +} + +function validateResource(d: any) { + const resourcePath: string = d[0]; + const resourceData: IResource = d[1]; + if (typeof (resourcePath) !== 'string') { + throw new Error('Expecting string for resourcePath'); + } + + if (typeof (resourceData) !== 'object' || + Object.keys(resourceData).length === 0 || + !('path' in resourceData) || + !('content' in resourceData) || + !('sha256sum' in resourceData)) { + throw new Error('Invalid datafileData object'); + } } // datafile Loading functions function loadUnpack(raw: string) { const dbDatafilesNew: any = {}; - - const bundle = JSON.parse(raw); + const dbResourcesNew: any = {}; const sha256hex = forgeMd.sha256.create().update(raw).digest().toHex(); + const bundle = JSON.parse(raw); - Object.entries(bundle).forEach((d) => { + Object.entries(bundle.data).forEach((d) => { validateDatafile(d); const datafilePath: string = d[0]; @@ -85,7 +121,19 @@ function loadUnpack(raw: string) { dbDatafilesNew[datafilePath] = datafileData; }); + Object.entries(bundle.resources).forEach((d) => { + validateResource(d); + + const resourcePath: string = d[0]; + const resourceData: any = d[1]; + + resourceData.path = resourcePath; + + dbResourcesNew[resourcePath] = resourceData; + }); + datafiles = dbDatafilesNew; + resources = dbResourcesNew; sha256sum = sha256hex; console.log(`End datafile reload: ${new Date()}`); diff --git a/src/schema.ts b/src/schema.ts index 1885f2f..862d788 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -132,18 +132,25 @@ const createSchemaType = function (schemaTypes: any, interfaceTypes: any, conf: fieldDef['type'] = t; - // schema if (fieldInfo.datafileSchema) { + // schema fieldDef['resolve'] = () => db.getDatafilesBySchema(fieldInfo.datafileSchema); - } - - // synthetic - if (fieldInfo.synthetic) { + } else if (fieldInfo.synthetic) { + // synthetic fieldDef['resolve'] = (root: any) => resolveSyntheticField( root, fieldInfo.synthetic.schema, fieldInfo.synthetic.subAttr, ); + } else if (fieldInfo.isResource) { + // resource + fieldDef['args'] = { path: { type: GraphQLString } }; + fieldDef['resolve'] = (root: any, args: any) => { + if (args.path) { + return [db.getResource(args.path)]; + } + return db.getResources(); + }; } // return @@ -197,6 +204,15 @@ const jsonType = new GraphQLScalarType({ serialize: JSON.stringify, }); +const resourceType = new GraphQLObjectType({ + name: 'Resource_v1', + fields: { + sha256sum: { type: new GraphQLNonNull(GraphQLString) }, + path: { type: new GraphQLNonNull(GraphQLString) }, + content: { type: new GraphQLNonNull(GraphQLString) }, + }, +}); + export function generateAppSchema(path: string): GraphQLSchema { const schemaData = yaml.safeLoad(fs.readFileSync(path, 'utf8')); diff --git a/test/schemas/cluster.data.json b/test/schemas/cluster.data.json index 838bc98..3e68cc6 100644 --- a/test/schemas/cluster.data.json +++ b/test/schemas/cluster.data.json @@ -1,9 +1,12 @@ { - "/cluster.yml": { - "name": "example cluster", - "labels": {}, - "serverUrl": "https://example.com", - "$schema": "/openshift/cluster-1.yml", - "description": "example cluster" + "resources": {}, + "data": { + "/cluster.yml": { + "name": "example cluster", + "labels": {}, + "serverUrl": "https://example.com", + "$schema": "/openshift/cluster-1.yml", + "description": "example cluster" + } } } diff --git a/test/schemas/cluster.test.ts b/test/schemas/cluster.test.ts index 2fe3434..84d54c1 100644 --- a/test/schemas/cluster.test.ts +++ b/test/schemas/cluster.test.ts @@ -7,7 +7,7 @@ import chaiHttp = require('chai-http'); chai.use(chaiHttp); const should = chai.should(); -describe('cluster', () => { +describe('clusters', () => { before(() => { db.loadFromFile('test/schemas/cluster.data.json'); }); @@ -15,10 +15,10 @@ describe('cluster', () => { it('serves a basic graphql query', (done) => { chai.request(server) .get('/graphql') - .query({ query: '{ cluster { name } }' }) + .query({ query: '{ clusters { name } }' }) .end((err, res) => { res.should.have.status(200); - res.body.data.cluster[0].name.should.equal('example cluster'); + res.body.data.clusters[0].name.should.equal('example cluster'); done(); }); }); diff --git a/test/server.data.json b/test/server.data.json index 641e0e6..f338303 100644 --- a/test/server.data.json +++ b/test/server.data.json @@ -1,51 +1,60 @@ { - "/role-A.yml": { - "$schema": "/access/role-1.yml", - "labels": {}, - "name": "role-A", - "permissions": [ - { - "$ref": "/permission-A.yml" - } - ] + "resources": { + "/resource1.yml": { + "path": "/resource1.yml", + "content": "test resource", + "sha256sum": "ff" + } }, - "/permission-A.yml": { - "$schema": "/access/permission-1.yml", - "name": "permission-A", - "service": "github-org-team", - "team": "team-A", - "org": "org-A", - "labels": {}, - "description": "description permission-A" - }, - "/app-A.yml": { - "$schema": "/app-sre/app-1.yml", - "labels": {}, - "title": "app-A", - "serviceOwner": { - "name": "serviceOwnerName", - "email": "test@example.com" + "data": { + "/role-A.yml": { + "$schema": "/access/role-1.yml", + "labels": {}, + "name": "role-A", + "permissions": [ + { + "$ref": "/permission-A.yml" + } + ] }, - "quayRepos": [ - { - "org": { - "$ref": "/quay-org-A.yml" - }, - "items": [ - { - "name": "repoA", - "description": "", - "public": true - } - ] - } - ] - }, - "/quay-org-A.yml": { - "$schema": "/dependencies/quay-org-1.yml", - "labels": {}, - "name": "quay-org-A", - "managedTeams": [ "teamA" ], - "description": "desc" + "/permission-A.yml": { + "$schema": "/access/permission-1.yml", + "name": "permission-A", + "service": "github-org-team", + "team": "team-A", + "org": "org-A", + "labels": {}, + "description": "description permission-A" + }, + "/app-A.yml": { + "$schema": "/app-sre/app-1.yml", + "labels": {}, + "title": "app-A", + "serviceOwner": { + "name": "serviceOwnerName", + "email": "test@example.com" + }, + "quayRepos": [ + { + "org": { + "$ref": "/quay-org-A.yml" + }, + "items": [ + { + "name": "repoA", + "description": "", + "public": true + } + ] + } + ] + }, + "/quay-org-A.yml": { + "$schema": "/dependencies/quay-org-1.yml", + "labels": {}, + "name": "quay-org-A", + "managedTeams": [ "teamA" ], + "description": "desc" + } } } diff --git a/test/server.test.ts b/test/server.test.ts index 464ae09..bb683d7 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -29,7 +29,7 @@ describe('server', () => { it('resolves item refs', (done) => { const query = `{ - role { + roles { name permissions { service @@ -42,7 +42,7 @@ describe('server', () => { .query({ query }) .end((err: any, res: any) => { validateGraphQLResponse(res); - const permissionsName: any = res.body.data.role[0].permissions[0].service; + const permissionsName: any = res.body.data.roles[0].permissions[0].service; permissionsName.should.equal('github-org-team'); done(); }); @@ -50,7 +50,7 @@ describe('server', () => { it('resolves object refs', (done) => { const query = `{ - app { + apps { quayRepos { org { name @@ -64,9 +64,29 @@ describe('server', () => { .query({ query }) .end((err: any, res: any) => { validateGraphQLResponse(res); - const orgResponse = res.body.data.app[0].quayRepos[0].org.name; + const orgResponse = res.body.data.apps[0].quayRepos[0].org.name; orgResponse.should.equal('quay-org-A'); done(); }); }); + + it('can retrieve a resource', (done) => { + const query = `{ + resources(path: "/resource1.yml") { + content + sha256sum + path + } + }`; + + chai.request(server) + .get('/graphql') + .query({ query }) + .end((err: any, res: any) => { + validateGraphQLResponse(res); + const resource = res.body.data.resources[0]; + resource.content.should.equal('test resource'); + done(); + }); + }); });