Skip to content

Commit

Permalink
Add architecture and os checks when fetching tags
Browse files Browse the repository at this point in the history
This updates the controller to check the architecture of the cluster
nodes and select only tags for the current architecture.

Signed-off-by: oluwole.fadeyi <[email protected]>
  • Loading branch information
oluwole.fadeyi committed Nov 16, 2020
1 parent 9bfbe1d commit 79273fa
Show file tree
Hide file tree
Showing 13 changed files with 443 additions and 48 deletions.
2 changes: 1 addition & 1 deletion deploy/yaml/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ metadata:
name: version-checker
rules:
- apiGroups: [""]
resources: ["pods"]
resources: ["pods","nodes"]
verbs: ["get", "watch", "list"]
---
kind: ClusterRoleBinding
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/aws/aws-sdk-go v1.34.10
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/google/go-cmp v0.4.1 // indirect
github.com/google/uuid v1.1.1
github.com/hashicorp/golang-lru v0.5.3 // indirect
github.com/imdario/mergo v0.3.9 // indirect
github.com/kr/text v0.2.0 // indirect
Expand Down
10 changes: 10 additions & 0 deletions pkg/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ const (

// PinPatchAnnotationKey will pin the patch version to check.
PinPatchAnnotationKey = "pin-patch.version-checker.io"

// PinArchAnnotationKey will only perform checkes on images of that arch
PinArchAnnotationKey = "pin-architecture.version-checker.io"

// PinOsAnnotationKey will only perform checkes on images of that arch
PinOsAnnotationKey = "pin-os.version-checker.io"
)

// Options is used to describe what restrictions should be used for determining
Expand All @@ -58,6 +64,10 @@ type Options struct {
PinPatch *int64 `json:"pin-patch,omitempty"`

RegexMatcher *regexp.Regexp `json:"-"`

// Architecture and OS to search for
Architecture *string `json:"pin-architecture,omitempty"`
OS *string `json:"pin-os,omitempty"`
}

// ImageTag describes a container image tag.
Expand Down
92 changes: 92 additions & 0 deletions pkg/controller/architecture/architecture.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package architecture

import (
"fmt"
"sync"

corev1 "k8s.io/api/core/v1"
)

var _ NodeArchitectureMap = &defaultNodeMap{}

const (
invalidFuncInput = "invalid function input"
)

// NodeMetadata metadata about a particular node
type NodeMetadata struct {
OS string
Architecture string
}

type NodeArchitectureMap interface {
GetNodeArchitecture(node string) (*NodeMetadata, error)
AddNode(node *corev1.Node) error
DeleteNode(node string) error
Length() int
}

type defaultNodeMap struct {
mu sync.RWMutex
nodes map[string]*NodeMetadata
}

func New() *defaultNodeMap {
// might need to pass an initial map
return &defaultNodeMap{
nodes: make(map[string]*NodeMetadata),
}
}

func (m *defaultNodeMap) GetNodeArchitecture(node string) (*NodeMetadata, error) {
m.mu.Lock()
defer m.mu.Unlock()

if _, ok := m.nodes[node]; !ok {
// no data about the node was found, return error
return nil, fmt.Errorf("error fetching node's architecture data")
}
return m.nodes[node], nil
}

func (m *defaultNodeMap) AddNode(node *corev1.Node) error {
m.mu.Lock()
defer m.mu.Unlock()
if node == nil {
return fmt.Errorf("add node %s", invalidFuncInput)
}

arch, ok := node.Labels["kubernetes.io/arch"]
if !ok {
return fmt.Errorf("\"kubernetes.io/arch\" label not found on node: %s", node.Name)
}

os, ok := node.Labels["kubernetes.io/os"]
if !ok {
return fmt.Errorf("\"kubernetes.io/os\" label not found on node: %s", node.Name)
}

// change name to uid or selflink
m.nodes[node.Name] = &NodeMetadata{
OS: os,
Architecture: arch,
}
return nil
}

func (m *defaultNodeMap) DeleteNode(node string) error {
m.mu.Lock()
defer m.mu.Unlock()
if node == "" {
return fmt.Errorf("delete node %s", invalidFuncInput)
}
// change name to uid or selflink
delete(m.nodes, node)
return nil
}

func (m *defaultNodeMap) Length() int {
m.mu.Lock()
defer m.mu.Unlock()
return len(m.nodes)
}
133 changes: 133 additions & 0 deletions pkg/controller/architecture/architecture_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package architecture

import (
"fmt"
"sync"
"testing"

"github.com/google/uuid"
corev1 "k8s.io/api/core/v1"
)

func TestAdd(t *testing.T) {
var wg sync.WaitGroup // using wait group to know if task was completed
var commonMap = New()
var expectedNumberOfNodes = 100

// Adding random nodes to the concurrent map
for i := 0; i < expectedNumberOfNodes; i++ {
wg.Add(1)
go func() {
defer wg.Done()
node, err := generateNode("node", "amd64", "linux")
if err != nil {
t.Errorf("unxepected error when generating the node info: %s", err)
}
err = commonMap.AddNode(node)
if err != nil {
t.Errorf("unxepected error when appending the node info: %s", err)
}
}()
}
wg.Wait()

// checking if all operations occurred without any issues on the locks
if commonMap.Length() != expectedNumberOfNodes {
t.Errorf("unxepected number of node information was found: exp=%d act=%d", expectedNumberOfNodes, commonMap.Length())
}

}

func TestRead(t *testing.T) {
var wg sync.WaitGroup // using wait group to know if task was completed
var commonMap = New()
var expectedNumberOfNodes = 1000
var nodes []*corev1.Node

for i := 0; i < expectedNumberOfNodes; i++ {
node, err := generateNode("node", "amd64", "linux")
if err != nil {
t.Errorf("unxepected error when generating the node info: %s", err)
}
err = commonMap.AddNode(node)
if err != nil {
t.Errorf("unxepected error when appending the node info: %s", err)
}
nodes = append(nodes, node)
}

for _, node := range nodes {
wg.Add(1)
go func() {
arch, err := commonMap.GetNodeArchitecture(node.Name)
if err != nil {
t.Errorf("unxepected error when reading node information: %s", err)
}
if expectedArch, ok := node.Labels["kubernetes.io/arch"]; ok {
if arch.Architecture != expectedArch {
t.Errorf("unxepected node architecture was found: exp=%q act=%q", expectedArch, arch.Architecture)
}
}
if expectedOS, ok := node.Labels["kubernetes.io/os"]; ok {
if arch.OS != expectedOS {
t.Errorf("unxepected node architecture was found: exp=%q act=%q", expectedOS, arch.OS)
}
}
wg.Done()
}()
}
wg.Wait()

if commonMap.Length() != expectedNumberOfNodes {
t.Errorf("unxepected number of node information was found: exp=%d act=%d", expectedNumberOfNodes, commonMap.Length())
}

}

func TestDelete(t *testing.T) {
var wg sync.WaitGroup // using wait group to know if task was completed
var commonMap = New()
var expectedNumberOfNodes = 1000
var nodes []*corev1.Node

for i := 0; i < expectedNumberOfNodes; i++ {
node, err := generateNode("node", "amd64", "linux")
if err != nil {
t.Errorf("unxepected error when generating the node info: %s", err)
}
err = commonMap.AddNode(node)
if err != nil {
t.Errorf("unxepected error when appending the node info: %s", err)
}
nodes = append(nodes, node)
}

for _, node := range nodes {
wg.Add(1)
go func() {
defer wg.Done()
err := commonMap.DeleteNode(node.Name)
if err != nil {
t.Errorf("unxepected error when deleting the node info: %s", err)
}
}()
wg.Wait()
}

// checking if all operations occurred without any issues on the locks
if commonMap.Length() != 0 {
t.Errorf("unxepected number of node information was found: exp=%d act=%d", 0, commonMap.Length())
}

}

func generateNode(name, arch, os string) (*corev1.Node, error) {
suffix, err := uuid.NewRandom()
node := &corev1.Node{}
node.Name = fmt.Sprintf("%s%d", name, suffix.ID())
node.Labels = map[string]string{
"kubernetes.io/arch": arch,
"kubernetes.io/os": os,
}
return node, err
}
23 changes: 22 additions & 1 deletion pkg/controller/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,49 @@ import (
corev1 "k8s.io/api/core/v1"

"github.com/jetstack/version-checker/pkg/api"
"github.com/jetstack/version-checker/pkg/controller/architecture"
"github.com/jetstack/version-checker/pkg/controller/search"
"github.com/jetstack/version-checker/pkg/version/semver"
"github.com/sirupsen/logrus"
)

type Checker struct {
search search.Searcher
nodes architecture.NodeArchitectureMap
}

type Result struct {
CurrentVersion string
LatestVersion string
IsLatest bool
ImageURL string
OS string
Architecture string
}

func New(search search.Searcher) *Checker {
func New(search search.Searcher, nodesArchInfo architecture.NodeArchitectureMap) *Checker {
return &Checker{
search: search,
nodes: nodesArchInfo,
}
}

// Container will return the result of the given container's current version, compared to the latest upstream
func (c *Checker) Container(ctx context.Context, log *logrus.Entry,
pod *corev1.Pod, container *corev1.Container, opts *api.Options) (*Result, error) {

if opts != nil {
// Get information about the pod node architecture
if opts.OS == nil || opts.Architecture == nil {
arch, err := c.nodes.GetNodeArchitecture(pod.Spec.NodeName)
if err != nil {
return nil, err
}
opts.OS = &arch.OS
opts.Architecture = &arch.Architecture
}
}

// If the container image SHA status is not ready yet, exit early
statusSHA := containerStatusImageSHA(pod, container.Name)
if len(statusSHA) == 0 {
Expand Down Expand Up @@ -90,6 +107,8 @@ func (c *Checker) Container(ctx context.Context, log *logrus.Entry,
LatestVersion: latestVersion,
IsLatest: isLatest,
ImageURL: imageURL,
OS: latestImage.OS,
Architecture: latestImage.Architecture,
}, nil
}

Expand Down Expand Up @@ -161,6 +180,8 @@ func (c *Checker) isLatestSHA(ctx context.Context, imageURL, currentSHA string,
LatestVersion: latestVersion,
IsLatest: isLatest,
ImageURL: imageURL,
OS: latestImage.OS,
Architecture: latestImage.Architecture,
}, nil
}

Expand Down
Loading

0 comments on commit 79273fa

Please sign in to comment.