From ae8f4f4269079fa9b388203297e478690ce6ae91 Mon Sep 17 00:00:00 2001 From: Adam Mohammed Date: Thu, 27 Jul 2023 09:37:38 -0400 Subject: [PATCH] graphql testin' --- .envrc | 3 + devenv.lock | 156 ++++++++++++++++++ devenv.nix | 5 + devenv.yaml | 3 + internal/metal/providers/emgql/client.go | 43 +++++ internal/metal/providers/emgql/mapping.go | 85 ++++++++++ .../metal/providers/emgql/organizations.go | 56 ++++++- internal/metal/providers/emgql/projects.go | 55 +++++- 8 files changed, 404 insertions(+), 2 deletions(-) create mode 100644 .envrc create mode 100644 devenv.lock create mode 100644 devenv.nix create mode 100644 devenv.yaml create mode 100644 internal/metal/providers/emgql/mapping.go diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..6de8a8a --- /dev/null +++ b/.envrc @@ -0,0 +1,3 @@ +source_url "https://raw.githubusercontent.com/cachix/devenv/d1f7b48e35e6dee421cfd0f51481d17f77586997/direnvrc" "sha256-YBzqskFZxmNb3kYVoKD9ZixoPXJh1C9ZvTLGFRkauZ0=" + +use devenv \ No newline at end of file diff --git a/devenv.lock b/devenv.lock new file mode 100644 index 0000000..f529fcb --- /dev/null +++ b/devenv.lock @@ -0,0 +1,156 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1690391043, + "narHash": "sha256-8x5DkOaejES6C2JYX2A3riebbJHGFHBmJ+LwxXUIIVw=", + "owner": "cachix", + "repo": "devenv", + "rev": "05240861ef3ae1bbe65b1acc88e2fad7dd24a84b", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1673956053, + "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1685518550, + "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "pre-commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1660459072, + "narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "a20de23b925fd8264fd7fad6454652e142fd7f73", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1690327932, + "narHash": "sha256-Fv7PYZxN4eo0K6zXhHG/vOc+e2iuqQ5ywDrh0yeRjP0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a9b47d85504bdd199e90846622c76aa0bfeabfac", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1685801374, + "narHash": "sha256-otaSUoFEMM+LjBI1XL/xGB5ao6IwnZOXc47qhIgJe8U=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c37ca420157f4abc31e26f436c1145f8951ff373", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-23.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ], + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1689668210, + "narHash": "sha256-XAATwDkaUxH958yXLs1lcEOmU6pSEIkatY3qjqk8X0E=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "eb433bff05b285258be76513add6f6c57b441775", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": "pre-commit-hooks" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/devenv.nix b/devenv.nix new file mode 100644 index 0000000..b746676 --- /dev/null +++ b/devenv.nix @@ -0,0 +1,5 @@ +{ pkgs, ... }: + +{ + languages.go.enable = true; +} diff --git a/devenv.yaml b/devenv.yaml new file mode 100644 index 0000000..c7cb5ce --- /dev/null +++ b/devenv.yaml @@ -0,0 +1,3 @@ +inputs: + nixpkgs: + url: github:NixOS/nixpkgs/nixpkgs-unstable diff --git a/internal/metal/providers/emgql/client.go b/internal/metal/providers/emgql/client.go index 819c3e8..78206ea 100644 --- a/internal/metal/providers/emgql/client.go +++ b/internal/metal/providers/emgql/client.go @@ -1,6 +1,11 @@ package emgql import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" "net/http" "net/url" "time" @@ -50,3 +55,41 @@ func New(options ...Option) (*Client, error) { return client, nil } + +func (c *Client) graphquery(ctx context.Context, q string, vars map[string]string) (io.ReadCloser, error) { + qObj := struct { + Query string `json:"query"` + Variables map[string]string `json:"variables"` + }{ + Query: q, + Variables: vars, + } + + res, err := json.Marshal(qObj) + if err != nil { + return nil, fmt.Errorf("failed to marshal request object: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL.String(), bytes.NewBuffer(res)) + if err != nil { + return nil, fmt.Errorf("failed to build post request: %w", err) + } + + req.Header.Add("content-type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to make post request: %w", err) + } + + if resp.StatusCode != http.StatusOK { + b, err2 := io.ReadAll(resp.Body) + if err2 != nil { + return nil, fmt.Errorf("got unexpected response code [%d]: unable to parse request body: %v", resp.StatusCode, err) + } + + return nil, fmt.Errorf("got unexpected response code [%d]: %s", resp.StatusCode, string(b)) + } + return resp.Body, nil + +} diff --git a/internal/metal/providers/emgql/mapping.go b/internal/metal/providers/emgql/mapping.go new file mode 100644 index 0000000..67aa19c --- /dev/null +++ b/internal/metal/providers/emgql/mapping.go @@ -0,0 +1,85 @@ +package emgql + +import "go.equinixmetal.net/infra9-metal-bridge/internal/metal/models" + +type organization struct { + ID string `json:"id"` + Name string `json:"name"` + Projects []project `json:"projects"` + Users []user `json:"users"` +} + +func (o organization) ToOrganizationDetails() models.OrganizationDetails { + orgDetails := models.OrganizationDetails{ + ID: o.ID, + Name: o.Name, + } + + memberships := []*models.Membership[models.OrganizationDetails]{} + for _, u := range o.Users { + m := u.ToOrganizationMembership() + memberships = append(memberships, &m) + } + + pDetails := []*models.ProjectDetails{} + for _, p := range o.Projects { + details := p.ToProjectDetails() + details.Organization = &orgDetails + pDetails = append(pDetails, &details) + } + + orgDetails.Memberships = memberships + orgDetails.Projects = pDetails + + return orgDetails + +} + +type project struct { + ID string `json:"id"` + Name string `json:"name"` + Users []user `json:"users"` + Organization struct { + ID string `json:"id"` + } `json:"organization"` +} + +func (p project) ToProjectDetails() models.ProjectDetails { + memberships := []*models.Membership[models.ProjectDetails]{} + for _, u := range p.Users { + m := u.ToProjectMembership() + memberships = append(memberships, &m) + } + + return models.ProjectDetails{ + ID: p.ID, + Name: p.Name, + Memberships: memberships, + Organization: &models.OrganizationDetails{ + ID: p.Organization.ID, + }, + } +} + +type user struct { + ID string `json:"id"` + Roles []string `json:"roles"` +} + +func (u user) ToOrganizationMembership() models.Membership[models.OrganizationDetails] { + return models.Membership[models.OrganizationDetails]{ + User: &models.UserDetails{ + ID: u.ID, + }, + Roles: u.Roles, + } +} + +func (u user) ToProjectMembership() models.Membership[models.ProjectDetails] { + return models.Membership[models.ProjectDetails]{ + User: &models.UserDetails{ + ID: u.ID, + }, + Roles: u.Roles, + } +} diff --git a/internal/metal/providers/emgql/organizations.go b/internal/metal/providers/emgql/organizations.go index 8338f21..4aff7b1 100644 --- a/internal/metal/providers/emgql/organizations.go +++ b/internal/metal/providers/emgql/organizations.go @@ -2,6 +2,9 @@ package emgql import ( "context" + "encoding/json" + "fmt" + "io" "go.infratographer.com/x/gidx" @@ -10,5 +13,56 @@ import ( // GetOrganizationDetails fetches the organization id provided with its memberships. func (c *Client) GetOrganizationDetails(ctx context.Context, id gidx.PrefixedID) (*models.OrganizationDetails, error) { - return nil, nil + q := ` +query ($orgId: OrgId!){ + organization(id: $orgId) { + id + name + users { + id + roles + } + projects { + id + name + } + } +} +` + variables := map[string]string{ + "orgId": id.String(), + } + + res, err := c.graphquery(ctx, q, variables) + if err != nil { + return nil, fmt.Errorf("emgql: error loading organization details: %w", err) + } + + org, err := organizationFromResponse(res) + if err != nil { + return nil, fmt.Errorf("emgql: failed to parse response: %w", err) + } + + out := org.ToOrganizationDetails() + return &out, nil +} + +func organizationFromResponse(r io.ReadCloser) (organization, error) { + out := struct { + Data struct { + Organization organization `json:"organization"` + } `json:"data"` + }{} + + body, err := io.ReadAll(r) + if err != nil { + return out.Data.Organization, fmt.Errorf("emgql: %w", err) + } + + err = json.Unmarshal(body, &out) + if err != nil { + return out.Data.Organization, fmt.Errorf("emgql: %w", err) + } + + return out.Data.Organization, nil } diff --git a/internal/metal/providers/emgql/projects.go b/internal/metal/providers/emgql/projects.go index 9b449d2..ef0c72e 100644 --- a/internal/metal/providers/emgql/projects.go +++ b/internal/metal/providers/emgql/projects.go @@ -2,6 +2,9 @@ package emgql import ( "context" + "encoding/json" + "fmt" + "io" "go.infratographer.com/x/gidx" @@ -10,5 +13,55 @@ import ( // GetProjectDetails fetchs the provided project id with membership information. func (c *Client) GetProjectDetails(ctx context.Context, id gidx.PrefixedID) (*models.ProjectDetails, error) { - return nil, nil + q := ` +query ($projectId: ProjectId!){ + project(id: $projectId) { + id + name + users { + id + roles + } + organization { + id + } + } +} +` + variables := map[string]string{ + "projectId": id.String(), + } + + res, err := c.graphquery(ctx, q, variables) + if err != nil { + return nil, fmt.Errorf("error loading project details: %w", err) + } + + prj, err := projectFromResponse(res) + if err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + out := prj.ToProjectDetails() + return &out, nil +} + +func projectFromResponse(r io.ReadCloser) (project, error) { + out := struct { + Data struct { + Project project `json:"project"` + } `json:"data"` + }{} + + body, err := io.ReadAll(r) + if err != nil { + return out.Data.Project, fmt.Errorf("emgql: %w", err) + } + + err = json.Unmarshal(body, &out) + if err != nil { + return out.Data.Project, fmt.Errorf("emgql: %w", err) + } + + return out.Data.Project, nil }