Skip to content

Commit

Permalink
✨ discover terraform in gitlab + overhaul discovery (#1975)
Browse files Browse the repository at this point in the history
Massive overhaul to the discovery code to make it clearer, reduce duplicate calls to the API, and make it ready for groups discovery.

It also now discovers Terraform assets, the next PR will have the terraform+git implementation to use these.

Signed-off-by: Dominik Richter <[email protected]>
  • Loading branch information
arlimus authored Sep 29, 2023
1 parent 64799d7 commit c5c8d7e
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 205 deletions.
27 changes: 0 additions & 27 deletions go.sum

Large diffs are not rendered by default.

28 changes: 14 additions & 14 deletions providers/gitlab/connection/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ type GitLabConnection struct {
group *gitlab.Group
project *gitlab.Project
projectID string // only used for initial setup, use project.ID afterwards!
groupPath string
projectPath string
groupName string
projectName string
client *gitlab.Client
}

Expand Down Expand Up @@ -60,8 +60,8 @@ func NewGitLabConnection(id uint32, asset *inventory.Asset, conf *inventory.Conf
Conf: conf,
id: id,
asset: asset,
groupPath: conf.Options["group"],
projectPath: conf.Options["project"],
groupName: conf.Options["group"],
projectName: conf.Options["project"],
projectID: conf.Options["project-id"],
client: client,
}
Expand Down Expand Up @@ -89,42 +89,42 @@ func (c *GitLabConnection) Group() (*gitlab.Group, error) {
if c.group != nil {
return c.group, nil
}
if c.groupPath == "" {
return nil, errors.New("cannot look up gitlab group, no group path defined")
if c.groupName == "" {
return nil, errors.New("cannot look up gitlab group, no group name defined")
}

var err error
c.group, _, err = c.Client().Groups.GetGroup(c.groupPath, nil)
c.group, _, err = c.Client().Groups.GetGroup(c.groupName, nil)
return c.group, err
}

func (c *GitLabConnection) IsGroup() bool {
return c.groupPath != ""
return c.groupName != ""
}

func (c *GitLabConnection) IsProject() bool {
return c.projectPath != "" || c.projectID != ""
return c.projectName != "" || c.projectID != ""
}

func (c *GitLabConnection) GID() (interface{}, error) {
if c.groupPath == "" {
if c.groupName == "" {
return nil, errors.New("cannot look up gitlab group, no group path defined")
}
return url.QueryEscape(c.groupPath), nil
return url.QueryEscape(c.groupName), nil
}

func (c *GitLabConnection) PID() (interface{}, error) {
if c.projectID != "" {
return c.projectID, nil
}

if c.groupPath == "" {
if c.groupName == "" {
return nil, errors.New("cannot look up gitlab group, no group path defined")
}
if c.projectPath == "" {
if c.projectName == "" {
return nil, errors.New("cannot look up gitlab project, no project path defined")
}
return url.QueryEscape(c.groupPath) + "/" + url.QueryEscape(c.projectPath), nil
return url.QueryEscape(c.groupName) + "/" + url.QueryEscape(c.projectName), nil
}

func (c *GitLabConnection) Project() (*gitlab.Project, error) {
Expand Down
1 change: 1 addition & 0 deletions providers/gitlab/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/stretchr/testify v1.8.4
github.com/xanzy/go-gitlab v0.91.1
go.mondoo.com/cnquery v0.0.0-20230920205842-55a158611de3
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
)

require (
Expand Down
2 changes: 2 additions & 0 deletions providers/gitlab/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
Expand Down
219 changes: 219 additions & 0 deletions providers/gitlab/provider/discovery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1

package provider

import (
"strconv"
"strings"

"github.com/rs/zerolog/log"
"github.com/xanzy/go-gitlab"
"go.mondoo.com/cnquery/providers-sdk/v1/inventory"
"go.mondoo.com/cnquery/providers/gitlab/connection"
"golang.org/x/exp/slices"
)

func (s *Service) discover(root *inventory.Asset, conn *connection.GitLabConnection) (*inventory.Inventory, error) {
if conn.Conf.Discover == nil {
return nil, nil
}

client := conn.Client()
if client == nil {
return nil, nil
}

assets := []*inventory.Asset{}
projects := []*gitlab.Project{}

targets := conn.Conf.Discover.Targets

// The following calls to discover Groups and Projects will always return
// gitlab.Group and gitlab.Project objects, no matter if we connect to only
// one system or many. This reduces code complexity.

groupAssets, groups, err := s.discoverGroups(root, conn)
if err != nil {
return nil, err
}
if slices.Contains(targets, DiscoveryGroup) {
assets = append(assets, groupAssets...)
}

projectAssets, projects, err := s.discoverProjects(root, conn, groups)
if err != nil {
return nil, err
}
if slices.Contains(targets, DiscoveryProject) {
assets = append(assets, projectAssets...)
}

if slices.Contains(targets, DiscoveryTerraform) {
repos, err := s.discoverTerraform(root, conn, projects)
if err != nil {
return nil, err
}
assets = append(assets, repos...)
}

if len(assets) == 0 {
return nil, nil
}
return &inventory.Inventory{
Spec: &inventory.InventorySpec{
Assets: assets,
},
}, nil
}

func (s *Service) discoverGroups(root *inventory.Asset, conn *connection.GitLabConnection) ([]*inventory.Asset, []*gitlab.Group, error) {
// If the root asset it a group, we are done because it's the returned
// main asset. If the root is a project, we want to additionally detect
// the group and return it.
// TODO: discover groups for generic gitlab connection
if conn.IsGroup() {
group, err := conn.Group()
return []*inventory.Asset{root}, []*gitlab.Group{group}, err
}

group, err := conn.Group()
if err != nil {
return nil, nil, err
}

conf := conn.Conf.Clone()
conf.Type = GitlabGroupConnection
conf.Options = map[string]string{
"group": group.Name,
"group-id": strconv.Itoa(group.ID),
}
asset := &inventory.Asset{
Connections: []*inventory.Config{conf},
}

s.detectAsGroup(asset, group)

return []*inventory.Asset{asset}, []*gitlab.Group{group}, nil
}

func (s *Service) discoverProjects(root *inventory.Asset, conn *connection.GitLabConnection, groups []*gitlab.Group) ([]*inventory.Asset, []*gitlab.Project, error) {
if conn.IsProject() {
project, err := conn.Project()
return []*inventory.Asset{root}, []*gitlab.Project{project}, err
}

var assets []*inventory.Asset
var projects []*gitlab.Project

for i := range groups {
group := groups[i]
groupProjects, err := discoverGroupProjects(conn, group.ID)
if err != nil {
return nil, nil, err
}

for j := range groupProjects {
project := groupProjects[j]
conf := conn.Conf.Clone()
conf.Type = GitlabProjectConnection
conf.Options = map[string]string{
"group": group.Name,
"group-id": strconv.Itoa(group.ID),
"project": project.Name,
"project-id": strconv.Itoa(project.ID),
}
asset := &inventory.Asset{
Name: project.NameWithNamespace,
Connections: []*inventory.Config{conf},
}

s.detectAsProject(asset, group, project)
if err != nil {
return nil, nil, err
}

assets = append(assets, asset)
projects = append(projects, project)
}
}
return assets, projects, nil
}

func discoverGroupProjects(conn *connection.GitLabConnection, gid interface{}) ([]*gitlab.Project, error) {
perPage := 50
page := 1
total := 50
projects := []*gitlab.Project{}
for page*perPage <= total {
projs, resp, err := conn.Client().Groups.ListGroupProjects(gid, &gitlab.ListGroupProjectsOptions{ListOptions: gitlab.ListOptions{Page: page, PerPage: perPage}})
if err != nil {
return nil, err
}
projects = append(projects, projs...)
total = resp.TotalItems
page += 1
}

return projects, nil
}

func (s *Service) discoverTerraform(root *inventory.Asset, conn *connection.GitLabConnection, projects []*gitlab.Project) ([]*inventory.Asset, error) {
var res []*inventory.Asset
for i := range projects {
project := projects[i]
files, err := discoverTerraformHcl(conn.Client(), project.ID)
if err != nil {
log.Error().Err(err).Msg("failed to discover terraform repo in gitlab")
} else if len(files) != 0 {
res = append(res, &inventory.Asset{
Connections: []*inventory.Config{{
Type: "terraform-hcl-git",
Options: map[string]string{
"ssh-url": project.SSHURLToRepo,
"http-url": project.HTTPURLToRepo,
},
Credentials: conn.Conf.Credentials,
}},
})
}
}
return res, nil
}

// discoverTerraformHcl will check if the repository contains terraform files and return the terraform asset
func discoverTerraformHcl(client *gitlab.Client, pid interface{}) ([]string, error) {
opts := &gitlab.ListTreeOptions{
ListOptions: gitlab.ListOptions{
PerPage: 100,
},
Recursive: gitlab.Bool(true),
}

nodes := []*gitlab.TreeNode{}
for {
data, resp, err := client.Repositories.ListTree(pid, opts)
if err != nil {
return nil, err
}
nodes = append(nodes, data...)

// Exit the loop when we've seen all pages.
if resp.NextPage == 0 {
break
}

// Update the page number to get the next page.
opts.Page = resp.NextPage
}

terraformFiles := []string{}
for i := range nodes {
node := nodes[i]
if node.Type == "blob" && strings.HasSuffix(node.Path, ".tf") {
terraformFiles = append(terraformFiles, node.Path)
}
}

return terraformFiles, nil
}
Loading

0 comments on commit c5c8d7e

Please sign in to comment.