github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/apiserver/facades/agent/uniter/goal-state_test.go (about)

     1  // Copyright 2018 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package uniter_test
     5  
     6  import (
     7  	"time"
     8  
     9  	"github.com/juju/errors"
    10  	jc "github.com/juju/testing/checkers"
    11  	gc "gopkg.in/check.v1"
    12  	"gopkg.in/juju/charm.v6"
    13  
    14  	"github.com/juju/juju/apiserver/common"
    15  	"github.com/juju/juju/apiserver/facade/facadetest"
    16  	"github.com/juju/juju/apiserver/facades/agent/uniter"
    17  	"github.com/juju/juju/apiserver/params"
    18  	apiservertesting "github.com/juju/juju/apiserver/testing"
    19  	"github.com/juju/juju/core/status"
    20  	"github.com/juju/juju/juju/testing"
    21  	"github.com/juju/juju/state"
    22  	coretesting "github.com/juju/juju/testing"
    23  	"github.com/juju/juju/testing/factory"
    24  )
    25  
    26  // uniterSuite implements common testing suite for all API
    27  // versions. It's not intended to be used directly or registered as a
    28  // suite, but embedded.
    29  type uniterGoalStateSuite struct {
    30  	testing.JujuConnSuite
    31  
    32  	authorizer apiservertesting.FakeAuthorizer
    33  	resources  *common.Resources
    34  	uniter     *uniter.UniterAPI
    35  
    36  	machine0      *state.Machine
    37  	machine1      *state.Machine
    38  	machine2      *state.Machine
    39  	logging       *state.Application
    40  	wordpress     *state.Application
    41  	wpCharm       *state.Charm
    42  	mysql         *state.Application
    43  	wordpressUnit *state.Unit
    44  	mysqlUnit     *state.Unit
    45  }
    46  
    47  var _ = gc.Suite(&uniterGoalStateSuite{})
    48  
    49  func (s *uniterGoalStateSuite) SetUpTest(c *gc.C) {
    50  	s.JujuConnSuite.SetUpTest(c)
    51  
    52  	// Create two machines, two applications and add a unit to each application.
    53  	s.machine0 = s.Factory.MakeMachine(c, &factory.MachineParams{
    54  		Series: "quantal",
    55  		Jobs:   []state.MachineJob{state.JobHostUnits, state.JobManageModel},
    56  	})
    57  	s.machine1 = s.Factory.MakeMachine(c, &factory.MachineParams{
    58  		Series: "quantal",
    59  		Jobs:   []state.MachineJob{state.JobHostUnits},
    60  	})
    61  	s.machine2 = s.Factory.MakeMachine(c, &factory.MachineParams{
    62  		Series: "quantal",
    63  		Jobs:   []state.MachineJob{state.JobHostUnits},
    64  	})
    65  
    66  	mysqlCharm := s.Factory.MakeCharm(c, &factory.CharmParams{
    67  		Name: "mysql",
    68  	})
    69  	s.mysql = s.Factory.MakeApplication(c, &factory.ApplicationParams{
    70  		Name:  "mysql",
    71  		Charm: mysqlCharm,
    72  	})
    73  
    74  	s.mysqlUnit = s.Factory.MakeUnit(c, &factory.UnitParams{
    75  		Application: s.mysql,
    76  		Machine:     s.machine1,
    77  	})
    78  
    79  	s.wpCharm = s.Factory.MakeCharm(c, &factory.CharmParams{
    80  		Name: "wordpress",
    81  		URL:  "cs:quantal/wordpress-0",
    82  	})
    83  	s.wordpress = s.Factory.MakeApplication(c, &factory.ApplicationParams{
    84  		Name:  "wordpress",
    85  		Charm: s.wpCharm,
    86  	})
    87  	s.wordpressUnit = s.Factory.MakeUnit(c, &factory.UnitParams{
    88  		Application: s.wordpress,
    89  		Machine:     s.machine0,
    90  	})
    91  
    92  	loggingCharm := s.Factory.MakeCharm(c, &factory.CharmParams{
    93  		Name: "logging",
    94  	})
    95  	s.logging = s.Factory.MakeApplication(c, &factory.ApplicationParams{
    96  		Name:  "logging",
    97  		Charm: loggingCharm,
    98  	})
    99  
   100  	// Create a FakeAuthorizer so we can check permissions,
   101  	// set up assuming unit 0 has logged in.
   102  	s.authorizer = apiservertesting.FakeAuthorizer{
   103  		Tag: s.mysql.Tag(),
   104  	}
   105  
   106  	// Create the resource registry separately to track invocations to
   107  	// Register.
   108  	s.resources = common.NewResources()
   109  	s.AddCleanup(func(_ *gc.C) { s.resources.StopAll() })
   110  
   111  	uniterAPI, err := uniter.NewUniterAPI(facadetest.Context{
   112  		State_:             s.State,
   113  		Resources_:         s.resources,
   114  		Auth_:              s.authorizer,
   115  		LeadershipChecker_: s.State.LeadershipChecker(),
   116  	})
   117  	c.Assert(err, jc.ErrorIsNil)
   118  	s.uniter = uniterAPI
   119  }
   120  
   121  var (
   122  	timestamp          = time.Date(2200, time.November, 5, 0, 0, 0, 0, time.UTC)
   123  	expectedUnitStatus = params.GoalStateStatus{
   124  		Status: "waiting",
   125  		Since:  &timestamp,
   126  	}
   127  	expectedRelationStatus = params.GoalStateStatus{
   128  		Status: "joining",
   129  		Since:  &timestamp,
   130  	}
   131  	expected2UnitsMysql = params.UnitsGoalState{
   132  		"mysql/0": expectedUnitStatus,
   133  		"mysql/1": expectedUnitStatus,
   134  	}
   135  	expectedUnitMysql = params.UnitsGoalState{
   136  		"mysql/0": expectedUnitStatus,
   137  	}
   138  )
   139  
   140  // TestGoalStatesNoRelation tests single application with single unit.
   141  func (s *uniterGoalStateSuite) TestGoalStatesNoRelation(c *gc.C) {
   142  	args := params.Entities{Entities: []params.Entity{
   143  		{Tag: "unit-mysql-0"},
   144  		{Tag: "unit-wordpress-0"},
   145  	}}
   146  	expected := params.GoalStateResults{
   147  		Results: []params.GoalStateResult{
   148  			{
   149  				Result: &params.GoalState{
   150  					Units: expectedUnitMysql,
   151  				},
   152  			}, {
   153  				Error: apiservertesting.ErrUnauthorized,
   154  			},
   155  		},
   156  	}
   157  	testGoalStates(c, s.uniter, args, expected)
   158  }
   159  
   160  // TestGoalStatesNoRelationTwoUnits adds a new unit to wordpress application.
   161  func (s *uniterGoalStateSuite) TestPeerUnitsNoRelation(c *gc.C) {
   162  	args := params.Entities{Entities: []params.Entity{
   163  		{Tag: "unit-mysql-0"},
   164  		{Tag: "unit-wordpress-0"},
   165  	}}
   166  
   167  	s.Factory.MakeUnit(c, &factory.UnitParams{
   168  		Application: s.mysql,
   169  		Machine:     s.machine1,
   170  	})
   171  
   172  	expected := params.GoalStateResults{
   173  		Results: []params.GoalStateResult{
   174  			{
   175  				Result: &params.GoalState{
   176  					Units: expected2UnitsMysql,
   177  				},
   178  			}, {
   179  				Error: apiservertesting.ErrUnauthorized,
   180  			},
   181  		},
   182  	}
   183  	testGoalStates(c, s.uniter, args, expected)
   184  }
   185  
   186  // TestGoalStatesSingleRelation tests structure with two different
   187  // application units and one relation between the units.
   188  func (s *uniterGoalStateSuite) TestGoalStatesSingleRelation(c *gc.C) {
   189  
   190  	err := s.addRelationEnterScope(c, s.mysqlUnit, "wordpress")
   191  	c.Assert(err, jc.ErrorIsNil)
   192  
   193  	args := params.Entities{Entities: []params.Entity{
   194  		{Tag: "unit-mysql-0"},
   195  		{Tag: "unit-wordpress-0"},
   196  	}}
   197  	expected := params.GoalStateResults{
   198  		Results: []params.GoalStateResult{
   199  			{
   200  				Result: &params.GoalState{
   201  					Units: expectedUnitMysql,
   202  					Relations: map[string]params.UnitsGoalState{
   203  						"db": {
   204  							"wordpress":   expectedRelationStatus,
   205  							"wordpress/0": expectedUnitStatus,
   206  						},
   207  					},
   208  				},
   209  			}, {
   210  				Error: apiservertesting.ErrUnauthorized,
   211  			},
   212  		},
   213  	}
   214  	testGoalStates(c, s.uniter, args, expected)
   215  }
   216  
   217  // TestGoalStatesDeadUnitsExcluded tests dead units should not show in the GoalState result.
   218  func (s *uniterGoalStateSuite) TestGoalStatesDeadUnitsExcluded(c *gc.C) {
   219  
   220  	err := s.addRelationEnterScope(c, s.wordpressUnit, "mysql")
   221  	c.Assert(err, jc.ErrorIsNil)
   222  
   223  	newMysqlUnit := s.Factory.MakeUnit(c, &factory.UnitParams{
   224  		Application: s.mysql,
   225  		Machine:     s.machine1,
   226  	})
   227  	args := params.Entities{Entities: []params.Entity{
   228  		{Tag: "unit-mysql-0"},
   229  	}}
   230  	testGoalStates(c, s.uniter, args, params.GoalStateResults{
   231  		Results: []params.GoalStateResult{
   232  			{
   233  				Result: &params.GoalState{
   234  					Units: expected2UnitsMysql,
   235  					Relations: map[string]params.UnitsGoalState{
   236  						"db": {
   237  							"wordpress":   expectedRelationStatus,
   238  							"wordpress/0": expectedUnitStatus,
   239  						},
   240  					},
   241  				},
   242  			},
   243  		},
   244  	})
   245  
   246  	err = newMysqlUnit.Destroy()
   247  	c.Assert(err, jc.ErrorIsNil)
   248  
   249  	testGoalStates(c, s.uniter, args, params.GoalStateResults{
   250  		Results: []params.GoalStateResult{
   251  			{
   252  				Result: &params.GoalState{
   253  					Units: params.UnitsGoalState{
   254  						"mysql/0": expectedUnitStatus,
   255  					},
   256  					Relations: map[string]params.UnitsGoalState{
   257  						"db": {
   258  							"wordpress":   expectedRelationStatus,
   259  							"wordpress/0": expectedUnitStatus,
   260  						},
   261  					},
   262  				},
   263  			},
   264  		},
   265  	})
   266  }
   267  
   268  // preventUnitDestroyRemove sets a non-allocating status on the unit, and hence
   269  // prevents it from being unceremoniously removed from state on Destroy. This
   270  // is useful because several tests go through a unit's lifecycle step by step,
   271  // asserting the behaviour of a given method in each state, and the unit quick-
   272  // remove change caused many of these to fail.
   273  func preventUnitDestroyRemove(c *gc.C, u *state.Unit) {
   274  	// To have a non-allocating status, a unit needs to
   275  	// be assigned to a machine.
   276  	_, err := u.AssignedMachineId()
   277  	if errors.IsNotAssigned(err) {
   278  		err = u.AssignToNewMachine()
   279  	}
   280  	c.Assert(err, jc.ErrorIsNil)
   281  	now := time.Now()
   282  	sInfo := status.StatusInfo{
   283  		Status:  status.Idle,
   284  		Message: "",
   285  		Since:   &now,
   286  	}
   287  	err = u.SetAgentStatus(sInfo)
   288  	c.Assert(err, jc.ErrorIsNil)
   289  }
   290  
   291  // TestGoalStatesSingleRelationDyingUnits tests dying units showing dying status in the GoalState result.
   292  func (s *uniterGoalStateSuite) TestGoalStatesSingleRelationDyingUnits(c *gc.C) {
   293  	mysqlUnit := s.Factory.MakeUnit(c, &factory.UnitParams{
   294  		Application: s.mysql,
   295  		Machine:     s.machine1,
   296  	})
   297  
   298  	err := s.addRelationEnterScope(c, mysqlUnit, "wordpress")
   299  	c.Assert(err, jc.ErrorIsNil)
   300  
   301  	args := params.Entities{Entities: []params.Entity{
   302  		{Tag: "unit-mysql-0"},
   303  	}}
   304  	testGoalStates(c, s.uniter, args, params.GoalStateResults{
   305  		Results: []params.GoalStateResult{
   306  			{
   307  				Result: &params.GoalState{
   308  					Units: expected2UnitsMysql,
   309  					Relations: map[string]params.UnitsGoalState{
   310  						"db": {
   311  							"wordpress":   expectedRelationStatus,
   312  							"wordpress/0": expectedUnitStatus,
   313  						},
   314  					},
   315  				},
   316  			},
   317  		},
   318  	})
   319  	preventUnitDestroyRemove(c, mysqlUnit)
   320  	err = mysqlUnit.Destroy()
   321  	c.Assert(err, jc.ErrorIsNil)
   322  
   323  	testGoalStates(c, s.uniter, args, params.GoalStateResults{
   324  		Results: []params.GoalStateResult{
   325  			{
   326  				Result: &params.GoalState{
   327  					Units: params.UnitsGoalState{
   328  						"mysql/0": expectedUnitStatus,
   329  						"mysql/1": params.GoalStateStatus{
   330  							Status: "dying",
   331  							Since:  &timestamp,
   332  						},
   333  					},
   334  					Relations: map[string]params.UnitsGoalState{
   335  						"db": {
   336  							"wordpress":   expectedRelationStatus,
   337  							"wordpress/0": expectedUnitStatus,
   338  						},
   339  					},
   340  				},
   341  			},
   342  		},
   343  	})
   344  }
   345  
   346  // TestGoalStatesCrossModelRelation tests remote relation application shows URL as key.
   347  func (s *uniterGoalStateSuite) TestGoalStatesCrossModelRelation(c *gc.C) {
   348  	err := s.addRelationEnterScope(c, s.mysqlUnit, "wordpress")
   349  	c.Assert(err, jc.ErrorIsNil)
   350  
   351  	args := params.Entities{Entities: []params.Entity{
   352  		{Tag: "unit-mysql-0"},
   353  	}}
   354  	testGoalStates(c, s.uniter, args, params.GoalStateResults{Results: []params.GoalStateResult{
   355  		{
   356  			Result: &params.GoalState{
   357  				Units: params.UnitsGoalState{
   358  					"mysql/0": expectedUnitStatus,
   359  				},
   360  				Relations: map[string]params.UnitsGoalState{
   361  					"db": {
   362  						"wordpress":   expectedRelationStatus,
   363  						"wordpress/0": expectedUnitStatus,
   364  					},
   365  				},
   366  			},
   367  		},
   368  	}})
   369  	_, err = s.State.AddRemoteApplication(state.AddRemoteApplicationParams{
   370  		Name:        "metrics-remote",
   371  		URL:         "ctrl1:admin/default.metrics",
   372  		SourceModel: coretesting.ModelTag,
   373  		Endpoints: []charm.Relation{{
   374  			Interface: "metrics",
   375  			Name:      "metrics",
   376  			Role:      charm.RoleProvider,
   377  			Scope:     charm.ScopeGlobal,
   378  		}},
   379  	})
   380  	c.Assert(err, jc.ErrorIsNil)
   381  	eps, err := s.State.InferEndpoints("mysql", "metrics-remote")
   382  	c.Assert(err, jc.ErrorIsNil)
   383  	_, err = s.State.AddRelation(eps...)
   384  	c.Assert(err, jc.ErrorIsNil)
   385  	testGoalStates(c, s.uniter, args, params.GoalStateResults{Results: []params.GoalStateResult{
   386  		{
   387  			Result: &params.GoalState{
   388  				Units: params.UnitsGoalState{
   389  					"mysql/0": expectedUnitStatus,
   390  				},
   391  				Relations: map[string]params.UnitsGoalState{
   392  					"db": {
   393  						"wordpress":   expectedRelationStatus,
   394  						"wordpress/0": expectedUnitStatus,
   395  					},
   396  					"metrics": {
   397  						"ctrl1:admin/default.metrics": expectedRelationStatus,
   398  					},
   399  				},
   400  			},
   401  		},
   402  	}})
   403  }
   404  
   405  // TestGoalStatesMultipleRelations tests GoalStates with three
   406  // applications one application has two units and each unit is related
   407  // to a different application unit.
   408  func (s *uniterGoalStateSuite) TestGoalStatesMultipleRelations(c *gc.C) {
   409  
   410  	// Add another wordpress unit on machine 1.
   411  	s.Factory.MakeUnit(c, &factory.UnitParams{
   412  		Application: s.wordpress,
   413  		Machine:     s.machine1,
   414  	})
   415  
   416  	mysqlCharm1 := s.Factory.MakeCharm(c, &factory.CharmParams{
   417  		Name: "mysql",
   418  	})
   419  	mysql1 := s.Factory.MakeApplication(c, &factory.ApplicationParams{
   420  		Name:  "mysql1",
   421  		Charm: mysqlCharm1,
   422  	})
   423  
   424  	mysqlUnit1 := s.Factory.MakeUnit(c, &factory.UnitParams{
   425  		Application: mysql1,
   426  		Machine:     s.machine2,
   427  	})
   428  
   429  	err := s.addRelationEnterScope(c, s.wordpressUnit, "mysql")
   430  	c.Assert(err, jc.ErrorIsNil)
   431  
   432  	err = s.addRelationEnterScope(c, s.mysqlUnit, "logging")
   433  	c.Assert(err, jc.ErrorIsNil)
   434  	err = s.addRelationEnterScope(c, mysqlUnit1, "logging")
   435  	c.Assert(err, jc.ErrorIsNil)
   436  
   437  	args := params.Entities{Entities: []params.Entity{
   438  		{Tag: "unit-mysql-0"},
   439  		{Tag: "unit-wordpress-0"},
   440  	}}
   441  
   442  	expected := params.GoalStateResults{
   443  		Results: []params.GoalStateResult{
   444  			{
   445  				Result: &params.GoalState{
   446  					Units: expectedUnitMysql,
   447  					Relations: map[string]params.UnitsGoalState{
   448  						"db": {
   449  							"wordpress":   expectedRelationStatus,
   450  							"wordpress/0": expectedUnitStatus,
   451  							"wordpress/1": expectedUnitStatus,
   452  						},
   453  						"info": {
   454  							"logging":   expectedRelationStatus,
   455  							"logging/0": expectedUnitStatus,
   456  						},
   457  					},
   458  				},
   459  			}, {
   460  				Error: apiservertesting.ErrUnauthorized,
   461  			},
   462  		},
   463  	}
   464  
   465  	testGoalStates(c, s.uniter, args, expected)
   466  }
   467  
   468  func (s *uniterGoalStateSuite) addRelationEnterScope(c *gc.C, unit1 *state.Unit, app2 string) error {
   469  	app1, err := unit1.Application()
   470  	c.Assert(err, jc.ErrorIsNil)
   471  
   472  	relation := s.addRelation(c, app1.Name(), app2)
   473  	relationUnit, err := relation.Unit(unit1)
   474  	c.Assert(err, jc.ErrorIsNil)
   475  
   476  	err = relationUnit.EnterScope(nil)
   477  	c.Assert(err, jc.ErrorIsNil)
   478  	s.assertInScope(c, relationUnit, true)
   479  	return err
   480  }
   481  
   482  func (s *uniterGoalStateSuite) addRelation(c *gc.C, first, second string) *state.Relation {
   483  	eps, err := s.State.InferEndpoints(first, second)
   484  	c.Assert(err, jc.ErrorIsNil)
   485  	rel, err := s.State.AddRelation(eps...)
   486  	c.Assert(err, jc.ErrorIsNil)
   487  	return rel
   488  }
   489  
   490  func (s *uniterGoalStateSuite) assertInScope(c *gc.C, relUnit *state.RelationUnit, inScope bool) {
   491  	ok, err := relUnit.InScope()
   492  	c.Assert(err, jc.ErrorIsNil)
   493  	c.Assert(ok, gc.Equals, inScope)
   494  }
   495  
   496  // goalStates call uniter.GoalStates API and compares the output with the
   497  // expected result.
   498  func testGoalStates(c *gc.C, thisUniter *uniter.UniterAPI, args params.Entities, expected params.GoalStateResults) {
   499  	result, err := thisUniter.GoalStates(args)
   500  	c.Assert(err, jc.ErrorIsNil)
   501  	for i := range result.Results {
   502  		if result.Results[i].Error != nil {
   503  			break
   504  		}
   505  		setSinceToNil(c, result.Results[i].Result)
   506  	}
   507  	c.Assert(err, jc.ErrorIsNil)
   508  	c.Assert(result, jc.DeepEquals, expected)
   509  }
   510  
   511  // setSinceToNil will set the field `since` to nil in order to
   512  // avoid a time check which otherwise would be impossible to pass.
   513  func setSinceToNil(c *gc.C, goalState *params.GoalState) {
   514  
   515  	for i, u := range goalState.Units {
   516  		c.Assert(u.Since, gc.NotNil)
   517  		u.Since = &timestamp
   518  		goalState.Units[i] = u
   519  	}
   520  	for endPoint, gs := range goalState.Relations {
   521  		for key, m := range gs {
   522  			c.Assert(m.Since, gc.NotNil)
   523  			m.Since = &timestamp
   524  			gs[key] = m
   525  		}
   526  		goalState.Relations[endPoint] = gs
   527  	}
   528  }