Skip to content

Commit

Permalink
Allow the idler to exclude some deployments from idling
Browse files Browse the repository at this point in the history
Adds a new api to enable/disable idler for a set of users.
Idler can be disbled for a user as follows

```
curl -H "Content-Type: application/json"  \
     -X POST  \
     --data '{"disable":["pradkuma-preview2"],"enable":[]}' \
  http://idler:8080/api/idler/userstatus/
```

Use `GET` to fetch the list of currently idled users

```
curl http://idler:8080/api/idler/userstatus/
```

Resolves
- fabric8-services#232
  • Loading branch information
pradeepti123 committed Feb 19, 2019
1 parent ce3b1d6 commit 1e985e5
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 47 deletions.
23 changes: 15 additions & 8 deletions cmd/fabric8-jenkins-idler/idler.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ type Idler struct {
tenantService tenant.Service
clusterView cluster.View
config configuration.Configuration
disabledUsers *model.StringSet
userIdlers *openshift.UserIdlerMap
}

// struct used to pass in cancelable task
Expand All @@ -47,12 +49,15 @@ type task struct {
}

// NewIdler creates a new instance of Idler. The configuration as well as feature toggle handler needs to be passed.
func NewIdler(features toggles.Features, tenantService tenant.Service, clusterView cluster.View, config configuration.Configuration) *Idler {
func NewIdler(features toggles.Features, tenantService tenant.Service, clusterView cluster.View,
config configuration.Configuration) *Idler {
return &Idler{
featureService: features,
tenantService: tenantService,
clusterView: clusterView,
config: config,
disabledUsers: model.NewStringSet(),
userIdlers: openshift.NewUserIdlerMap(),
}
}

Expand All @@ -73,16 +78,17 @@ func (idler *Idler) Run() {
func (idler *Idler) startWorkers(t *task, addProfiler bool) {
idlerLogger.Info("Starting all Idler workers")

// Create synchronized map for UserIdler instances
userIdlers := openshift.NewUserIdlerMap()

// Start the controllers to monitor the OpenShift clusters
idler.watchOpenshiftEvents(t, userIdlers)
idler.watchOpenshiftEvents(t)

// Start API router
go func() {
// Create and start a Router instance to serve the REST API
idlerAPI := api.NewIdlerAPI(userIdlers, idler.clusterView, idler.tenantService)
idlerAPI := api.NewIdlerAPI(
idler.userIdlers,
idler.clusterView,
idler.tenantService,
idler.disabledUsers)
apirouter := router.CreateAPIRouter(idlerAPI)
router := router.NewRouter(apirouter)
router.AddMetrics(apirouter)
Expand All @@ -98,7 +104,7 @@ func (idler *Idler) startWorkers(t *task, addProfiler bool) {
}
}

func (idler *Idler) watchOpenshiftEvents(t *task, userIdlers *openshift.UserIdlerMap) {
func (idler *Idler) watchOpenshiftEvents(t *task) {
oc := client.NewOpenShift()

for _, c := range idler.clusterView.GetClusters() {
Expand All @@ -107,12 +113,13 @@ func (idler *Idler) watchOpenshiftEvents(t *task, userIdlers *openshift.UserIdle
t.ctx,
c.APIURL,
c.Token,
userIdlers,
idler.userIdlers,
idler.tenantService,
idler.featureService,
idler.config,
t.wg,
t.cancel,
idler.disabledUsers,
)

t.wg.Add(2)
Expand Down
59 changes: 43 additions & 16 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/fabric8-services/fabric8-jenkins-idler/internal/openshift"
"github.com/fabric8-services/fabric8-jenkins-idler/internal/openshift/client"
"github.com/fabric8-services/fabric8-jenkins-idler/internal/tenant"

"github.com/fabric8-services/fabric8-jenkins-idler/metric"
"github.com/julienschmidt/httprouter"
log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -51,37 +52,50 @@ type IdlerAPI interface {
// If an error occurs a response with the HTTP status 400 or 500 is returned.
Status(w http.ResponseWriter, r *http.Request, ps httprouter.Params)

// Info writes a JSON representation of internal state of the specified namespace to the response writer.
Info(w http.ResponseWriter, r *http.Request, ps httprouter.Params)

// ClusterDNSView writes a JSON representation of the current cluster state to the response writer.
ClusterDNSView(w http.ResponseWriter, r *http.Request, ps httprouter.Params)

// Reset deletes a pod and starts a new one
Reset(w http.ResponseWriter, r *http.Request, ps httprouter.Params)

// SetUserIdlerStatus set users status for idler.
SetUserIdlerStatus(w http.ResponseWriter, r *http.Request, ps httprouter.Params)

// GetDisabledUserIdlers gets the user status for idler.
GetDisabledUserIdlers(w http.ResponseWriter, r *http.Request, ps httprouter.Params)
}

type idler struct {
userIdlers *openshift.UserIdlerMap
clusterView cluster.View
openShiftClient client.OpenShiftClient
controller openshift.Controller
tenantService tenant.Service
disabledUsers *model.StringSet
}

type status struct {
IsIdle bool `json:"is_idle"`
}

type userStatus struct {
Disable []string `json:"disable"`
Enable []string `json:"enable"`
}

// NewIdlerAPI creates a new instance of IdlerAPI.
func NewIdlerAPI(userIdlers *openshift.UserIdlerMap, clusterView cluster.View, ts tenant.Service) IdlerAPI {
func NewIdlerAPI(
userIdlers *openshift.UserIdlerMap,
clusterView cluster.View,
ts tenant.Service,
du *model.StringSet) IdlerAPI {
// Initialize metrics
Recorder.Initialize()
return &idler{
userIdlers: userIdlers,
clusterView: clusterView,
openShiftClient: client.NewOpenShift(),
tenantService: ts,
disabledUsers: du,
}
}

Expand Down Expand Up @@ -208,17 +222,6 @@ func (api *idler) Status(w http.ResponseWriter, r *http.Request, ps httprouter.P
writeResponse(w, http.StatusOK, *response)
}

func (api *idler) Info(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
namespace := ps.ByName("namespace")

userIdler, ok := api.userIdlers.Load(namespace)
if ok {
writeResponse(w, http.StatusOK, userIdler.GetUser())
} else {
respondWithError(w, http.StatusNotFound, fmt.Errorf("Could not find queried namespace"))
}
}

func (api *idler) ClusterDNSView(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
writeResponse(w, http.StatusOK, api.clusterView.GetDNSView())
}
Expand Down Expand Up @@ -246,6 +249,30 @@ func (api *idler) Reset(w http.ResponseWriter, r *http.Request, ps httprouter.Pa
w.WriteHeader(http.StatusOK)
}

//SetUserIdlerStatus sets the user status
func (api *idler) SetUserIdlerStatus(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
var users userStatus
if err := json.NewDecoder(r.Body).Decode(&users); err != nil {
respondWithError(w, http.StatusBadRequest, err)
return
}

// enabled users will take precedence over disabled
api.disabledUsers.Add(users.Disable)
api.disabledUsers.Remove(users.Enable)
w.WriteHeader(http.StatusOK)
}

type idlerStatusResponse struct {
Users []string `json:"users,omitempty"`
}

//GetDisabledUserIdlers set the user status
func (api *idler) GetDisabledUserIdlers(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
users := &idlerStatusResponse{Users: api.disabledUsers.Keys()}
writeResponse(w, http.StatusOK, users)
}

func (api *idler) getURLAndToken(r *http.Request) (string, string, error) {
var openShiftAPIURL string
values, ok := r.URL.Query()[OpenShiftAPIParam]
Expand Down
29 changes: 29 additions & 0 deletions internal/model/stringset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package model

import (
cmap "github.com/orcaman/concurrent-map"
)

// StringSet is a type-safe and concurrent map keeping track of disabled v for idler.
type StringSet struct {
cmap.ConcurrentMap
}

// NewStringSet creates a new instance of StringSet map.
func NewStringSet() *StringSet {
return &StringSet{cmap.New()}
}

// Add stores the specified value under the key user.
func (m *StringSet) Add(vs []string) {
for _, v := range vs {
m.ConcurrentMap.Set(v, true)
}
}

// Remove deletes the specified disabled user from the map.
func (m *StringSet) Remove(vs []string) {
for _, v := range vs {
m.ConcurrentMap.Remove(v)
}
}
61 changes: 61 additions & 0 deletions internal/model/stringset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package model

import (
"sort"
"testing"

"github.com/stretchr/testify/assert"
)

func TestStringSet_new(t *testing.T) {
s := NewStringSet()
assert.NotNil(t, s, "must return a new object")
}

func TestStringSet_count(t *testing.T) {
s := NewStringSet()
assert.Equal(t, 0, s.Count(), "must have 0 items")
}

func TestStringSet_add(t *testing.T) {
s := NewStringSet()
s.Add([]string{"foo", "bar"})
assert.Equal(t, 2, s.Count())
}

func TestStringSet_has(t *testing.T) {
s := NewStringSet()
assert.False(t, s.Has("foo"), "must be empty")
s.Add([]string{"foo", "bar"})

assert.True(t, s.Has("foo"), "must be present after adding")
assert.True(t, s.Has("bar"), "must be present after adding")
assert.False(t, s.Has("foobar"), "non existent key seems to be found")
}

func TestStringSet_remove(t *testing.T) {
s := NewStringSet()
assert.Equal(t, 0, s.Count())

s.Add([]string{"foo", "bar"})
assert.Equal(t, 2, s.Count())

s.Remove([]string{"bar"})
assert.Equal(t, 1, s.Count())

assert.False(t, s.Has("bar"), "key that got removed still exists")
}

func TestStringSet_keys(t *testing.T) {
s := NewStringSet()

keys := []string{"foo", "bar"}

s.Add(keys)

values := s.Keys()
sort.Strings(keys)
sort.Strings(values)

assert.Equal(t, keys, values)
}
19 changes: 17 additions & 2 deletions internal/openshift/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import (
"github.com/sirupsen/logrus"
)

// Status of Users
type Status bool

const (
availableCond = "Available"
channelSendTimeout = 1
Expand Down Expand Up @@ -44,6 +47,7 @@ type controllerImpl struct {
ctx context.Context
cancel context.CancelFunc
unknownUsers *UnknownUsersMap
disabledUsers *model.StringSet
}

// NewController creates an instance of controllerImpl.
Expand All @@ -54,7 +58,9 @@ func NewController(
t tenant.Service,
features toggles.Features,
config configuration.Configuration,
wg *sync.WaitGroup, cancel context.CancelFunc) Controller {
wg *sync.WaitGroup,
cancel context.CancelFunc,
disabledUsers *model.StringSet) Controller {

logger.WithField("cluster", openshiftURL).Info("Creating new controller instance")

Expand All @@ -69,6 +75,7 @@ func NewController(
ctx: ctx,
cancel: cancel,
unknownUsers: NewUnknownUsersMap(),
disabledUsers: disabledUsers,
}

return &controller
Expand All @@ -85,7 +92,6 @@ func (c *controllerImpl) HandleBuild(o model.Object) error {
"event": "build",
"openshift": c.openshiftURL,
})

ok, err := c.createIfNotExist(ns)
if err != nil {
log.Errorf("Creating user-idler record failed: %s", err)
Expand All @@ -104,6 +110,10 @@ func (c *controllerImpl) HandleBuild(o model.Object) error {
"name": user.Name,
})

if c.disabledUsers.Has(user.Name) {
log.Infof("Status disabled for user: %s", user.Name)
return nil
}
evalConditions := false

if c.isActive(&o.Object) {
Expand Down Expand Up @@ -174,6 +184,11 @@ func (c *controllerImpl) HandleDeploymentConfig(dc model.DCObject) error {
userIdler := c.userIdlerForNamespace(ns)
user := userIdler.GetUser()

if c.disabledUsers.Has(user.Name) {
log.Infof("Status disabled for user: %s", user.Name)
return nil
}

log = log.WithFields(logrus.Fields{
"id": user.ID,
"name": user.Name,
Expand Down
3 changes: 2 additions & 1 deletion internal/openshift/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,8 @@ func setUp(t *testing.T) {
defer cancel()

userIdlers := NewUserIdlerMap()
controller = NewController(ctx, "", "", userIdlers, tenantService, features, &mock.Config{}, &wg, cancel)
disabledUsers := model.NewStringSet()
controller = NewController(ctx, "", "", userIdlers, tenantService, features, &mock.Config{}, &wg, cancel, disabledUsers)
}

func emptyChannel(ch chan model.User) {
Expand Down
9 changes: 6 additions & 3 deletions internal/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,6 @@ func (r *Router) Shutdown() {
func CreateAPIRouter(api api.IdlerAPI) *httprouter.Router {
router := httprouter.New()

router.GET("/api/idler/info/:namespace", api.Info)
router.GET("/api/idler/info/:namespace/", api.Info)

router.GET("/api/idler/idle/:namespace", api.Idle)
router.GET("/api/idler/idle/:namespace/", api.Idle)

Expand All @@ -107,5 +104,11 @@ func CreateAPIRouter(api api.IdlerAPI) *httprouter.Router {
router.POST("/api/idler/reset/:namespace", api.Reset)
router.POST("/api/idler/reset/:namespace/", api.Reset)

router.GET("/api/idler/userstatus", api.GetDisabledUserIdlers)
router.GET("/api/idler/userstatus/", api.GetDisabledUserIdlers)

router.POST("/api/idler/userstatus", api.SetUserIdlerStatus)
router.POST("/api/idler/userstatus/", api.SetUserIdlerStatus)

return router
}
Loading

0 comments on commit 1e985e5

Please sign in to comment.