github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/daemon/api_interfaces_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2021 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package daemon_test 21 22 import ( 23 "bytes" 24 "encoding/json" 25 "fmt" 26 "net/http" 27 "net/http/httptest" 28 "strings" 29 30 "gopkg.in/check.v1" 31 32 "github.com/snapcore/snapd/client" 33 "github.com/snapcore/snapd/daemon" 34 "github.com/snapcore/snapd/interfaces" 35 "github.com/snapcore/snapd/interfaces/builtin" 36 "github.com/snapcore/snapd/interfaces/ifacetest" 37 "github.com/snapcore/snapd/overlord/ifacestate" 38 "github.com/snapcore/snapd/overlord/state" 39 ) 40 41 var _ = check.Suite(&interfacesSuite{}) 42 43 type interfacesSuite struct { 44 apiBaseSuite 45 } 46 47 func (s *interfacesSuite) SetUpTest(c *check.C) { 48 s.apiBaseSuite.SetUpTest(c) 49 50 s.expectWriteAccess(daemon.AuthenticatedAccess{Polkit: "io.snapcraft.snapd.manage-interfaces"}) 51 } 52 53 func mockIface(c *check.C, d *daemon.Daemon, iface interfaces.Interface) { 54 err := d.Overlord().InterfaceManager().Repository().AddInterface(iface) 55 c.Assert(err, check.IsNil) 56 } 57 58 // inverseCaseMapper implements SnapMapper to use lower case internally and upper case externally. 59 type inverseCaseMapper struct { 60 ifacestate.IdentityMapper // Embed the identity mapper to reuse empty state mapping functions. 61 } 62 63 func (m *inverseCaseMapper) RemapSnapFromRequest(snapName string) string { 64 return strings.ToLower(snapName) 65 } 66 67 func (m *inverseCaseMapper) RemapSnapToResponse(snapName string) string { 68 return strings.ToUpper(snapName) 69 } 70 71 func (m *inverseCaseMapper) SystemSnapName() string { 72 return "core" 73 } 74 75 // Tests for POST /v2/interfaces 76 77 const ( 78 consumerYaml = ` 79 name: consumer 80 version: 1 81 apps: 82 app: 83 plugs: 84 plug: 85 interface: test 86 key: value 87 label: label 88 ` 89 90 producerYaml = ` 91 name: producer 92 version: 1 93 apps: 94 app: 95 slots: 96 slot: 97 interface: test 98 key: value 99 label: label 100 ` 101 102 coreProducerYaml = ` 103 name: core 104 version: 1 105 slots: 106 slot: 107 interface: test 108 key: value 109 label: label 110 ` 111 112 differentProducerYaml = ` 113 name: producer 114 version: 1 115 apps: 116 app: 117 slots: 118 slot: 119 interface: different 120 key: value 121 label: label 122 ` 123 ) 124 125 func (s *interfacesSuite) TestConnectPlugSuccess(c *check.C) { 126 restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 127 defer restore() 128 // Install an inverse case mapper to exercise the interface mapping at the same time. 129 restore = ifacestate.MockSnapMapper(&inverseCaseMapper{}) 130 defer restore() 131 132 d := s.daemon(c) 133 134 s.mockSnap(c, consumerYaml) 135 s.mockSnap(c, producerYaml) 136 137 d.Overlord().Loop() 138 defer d.Overlord().Stop() 139 140 action := &client.InterfaceAction{ 141 Action: "connect", 142 Plugs: []client.Plug{{Snap: "CONSUMER", Name: "plug"}}, 143 Slots: []client.Slot{{Snap: "PRODUCER", Name: "slot"}}, 144 } 145 text, err := json.Marshal(action) 146 c.Assert(err, check.IsNil) 147 buf := bytes.NewBuffer(text) 148 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 149 c.Assert(err, check.IsNil) 150 rec := httptest.NewRecorder() 151 s.req(c, req, nil).ServeHTTP(rec, req) 152 c.Check(rec.Code, check.Equals, 202) 153 var body map[string]interface{} 154 err = json.Unmarshal(rec.Body.Bytes(), &body) 155 c.Check(err, check.IsNil) 156 id := body["change"].(string) 157 158 st := d.Overlord().State() 159 st.Lock() 160 chg := st.Change(id) 161 st.Unlock() 162 c.Assert(chg, check.NotNil) 163 164 <-chg.Ready() 165 166 st.Lock() 167 err = chg.Err() 168 st.Unlock() 169 c.Assert(err, check.IsNil) 170 171 repo := d.Overlord().InterfaceManager().Repository() 172 ifaces := repo.Interfaces() 173 c.Assert(ifaces.Connections, check.HasLen, 1) 174 c.Check(ifaces.Connections, check.DeepEquals, []*interfaces.ConnRef{{ 175 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 176 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 177 }}) 178 } 179 180 func (s *interfacesSuite) TestConnectPlugFailureInterfaceMismatch(c *check.C) { 181 d := s.daemon(c) 182 183 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"}) 184 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "different"}) 185 s.mockSnap(c, consumerYaml) 186 s.mockSnap(c, differentProducerYaml) 187 188 action := &client.InterfaceAction{ 189 Action: "connect", 190 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 191 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 192 } 193 text, err := json.Marshal(action) 194 c.Assert(err, check.IsNil) 195 buf := bytes.NewBuffer(text) 196 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 197 c.Assert(err, check.IsNil) 198 rec := httptest.NewRecorder() 199 s.req(c, req, nil).ServeHTTP(rec, req) 200 c.Check(rec.Code, check.Equals, 400) 201 var body map[string]interface{} 202 err = json.Unmarshal(rec.Body.Bytes(), &body) 203 c.Check(err, check.IsNil) 204 c.Check(body, check.DeepEquals, map[string]interface{}{ 205 "result": map[string]interface{}{ 206 "message": "cannot connect consumer:plug (\"test\" interface) to producer:slot (\"different\" interface)", 207 }, 208 "status": "Bad Request", 209 "status-code": 400.0, 210 "type": "error", 211 }) 212 repo := d.Overlord().InterfaceManager().Repository() 213 ifaces := repo.Interfaces() 214 c.Assert(ifaces.Connections, check.HasLen, 0) 215 } 216 217 func (s *interfacesSuite) TestConnectPlugFailureNoSuchPlug(c *check.C) { 218 d := s.daemon(c) 219 220 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"}) 221 // there is no consumer, no plug defined 222 s.mockSnap(c, producerYaml) 223 s.mockSnap(c, consumerYaml) 224 225 action := &client.InterfaceAction{ 226 Action: "connect", 227 Plugs: []client.Plug{{Snap: "consumer", Name: "missingplug"}}, 228 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 229 } 230 text, err := json.Marshal(action) 231 c.Assert(err, check.IsNil) 232 buf := bytes.NewBuffer(text) 233 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 234 c.Assert(err, check.IsNil) 235 rec := httptest.NewRecorder() 236 s.req(c, req, nil).ServeHTTP(rec, req) 237 c.Check(rec.Code, check.Equals, 400) 238 239 var body map[string]interface{} 240 err = json.Unmarshal(rec.Body.Bytes(), &body) 241 c.Check(err, check.IsNil) 242 c.Check(body, check.DeepEquals, map[string]interface{}{ 243 "result": map[string]interface{}{ 244 "message": "snap \"consumer\" has no plug named \"missingplug\"", 245 }, 246 "status": "Bad Request", 247 "status-code": 400.0, 248 "type": "error", 249 }) 250 251 repo := d.Overlord().InterfaceManager().Repository() 252 ifaces := repo.Interfaces() 253 c.Assert(ifaces.Connections, check.HasLen, 0) 254 } 255 256 func (s *interfacesSuite) TestConnectAlreadyConnected(c *check.C) { 257 d := s.daemon(c) 258 259 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"}) 260 // there is no consumer, no plug defined 261 s.mockSnap(c, producerYaml) 262 s.mockSnap(c, consumerYaml) 263 264 repo := d.Overlord().InterfaceManager().Repository() 265 connRef := &interfaces.ConnRef{ 266 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 267 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 268 } 269 270 d.Overlord().Loop() 271 defer d.Overlord().Stop() 272 273 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 274 c.Assert(err, check.IsNil) 275 conns := map[string]interface{}{ 276 "consumer:plug producer:slot": map[string]interface{}{ 277 "auto": false, 278 }, 279 } 280 st := d.Overlord().State() 281 st.Lock() 282 st.Set("conns", conns) 283 st.Unlock() 284 285 action := &client.InterfaceAction{ 286 Action: "connect", 287 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 288 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 289 } 290 text, err := json.Marshal(action) 291 c.Assert(err, check.IsNil) 292 buf := bytes.NewBuffer(text) 293 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 294 c.Assert(err, check.IsNil) 295 rec := httptest.NewRecorder() 296 s.req(c, req, nil).ServeHTTP(rec, req) 297 c.Check(rec.Code, check.Equals, 202) 298 var body map[string]interface{} 299 err = json.Unmarshal(rec.Body.Bytes(), &body) 300 c.Check(err, check.IsNil) 301 id := body["change"].(string) 302 303 st.Lock() 304 chg := st.Change(id) 305 c.Assert(chg.Tasks(), check.HasLen, 0) 306 c.Assert(chg.Status(), check.Equals, state.DoneStatus) 307 st.Unlock() 308 } 309 310 func (s *interfacesSuite) TestConnectPlugFailureNoSuchSlot(c *check.C) { 311 d := s.daemon(c) 312 313 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"}) 314 s.mockSnap(c, consumerYaml) 315 s.mockSnap(c, producerYaml) 316 // there is no producer, no slot defined 317 318 action := &client.InterfaceAction{ 319 Action: "connect", 320 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 321 Slots: []client.Slot{{Snap: "producer", Name: "missingslot"}}, 322 } 323 text, err := json.Marshal(action) 324 c.Assert(err, check.IsNil) 325 buf := bytes.NewBuffer(text) 326 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 327 c.Assert(err, check.IsNil) 328 rec := httptest.NewRecorder() 329 s.req(c, req, nil).ServeHTTP(rec, req) 330 c.Check(rec.Code, check.Equals, 400) 331 332 var body map[string]interface{} 333 err = json.Unmarshal(rec.Body.Bytes(), &body) 334 c.Check(err, check.IsNil) 335 c.Check(body, check.DeepEquals, map[string]interface{}{ 336 "result": map[string]interface{}{ 337 "message": "snap \"producer\" has no slot named \"missingslot\"", 338 }, 339 "status": "Bad Request", 340 "status-code": 400.0, 341 "type": "error", 342 }) 343 344 repo := d.Overlord().InterfaceManager().Repository() 345 ifaces := repo.Interfaces() 346 c.Assert(ifaces.Connections, check.HasLen, 0) 347 } 348 349 func (s *interfacesSuite) testConnectFailureNoSnap(c *check.C, installedSnap string) { 350 // sanity, either consumer or producer needs to be enabled 351 consumer := installedSnap == "consumer" 352 producer := installedSnap == "producer" 353 c.Assert(consumer || producer, check.Equals, true, check.Commentf("installed snap must be consumer or producer")) 354 355 d := s.daemon(c) 356 357 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"}) 358 359 if consumer { 360 s.mockSnap(c, consumerYaml) 361 } 362 if producer { 363 s.mockSnap(c, producerYaml) 364 } 365 366 action := &client.InterfaceAction{ 367 Action: "connect", 368 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 369 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 370 } 371 text, err := json.Marshal(action) 372 c.Assert(err, check.IsNil) 373 buf := bytes.NewBuffer(text) 374 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 375 c.Assert(err, check.IsNil) 376 rec := httptest.NewRecorder() 377 s.req(c, req, nil).ServeHTTP(rec, req) 378 c.Check(rec.Code, check.Equals, 400) 379 380 var body map[string]interface{} 381 err = json.Unmarshal(rec.Body.Bytes(), &body) 382 c.Check(err, check.IsNil) 383 if producer { 384 c.Check(body, check.DeepEquals, map[string]interface{}{ 385 "result": map[string]interface{}{ 386 "message": "snap \"consumer\" is not installed", 387 }, 388 "status": "Bad Request", 389 "status-code": 400.0, 390 "type": "error", 391 }) 392 } else { 393 c.Check(body, check.DeepEquals, map[string]interface{}{ 394 "result": map[string]interface{}{ 395 "message": "snap \"producer\" is not installed", 396 }, 397 "status": "Bad Request", 398 "status-code": 400.0, 399 "type": "error", 400 }) 401 } 402 } 403 404 func (s *interfacesSuite) TestConnectPlugFailureNoPlugSnap(c *check.C) { 405 s.testConnectFailureNoSnap(c, "producer") 406 } 407 408 func (s *interfacesSuite) TestConnectPlugFailureNoSlotSnap(c *check.C) { 409 s.testConnectFailureNoSnap(c, "consumer") 410 } 411 412 func (s *interfacesSuite) TestConnectPlugChangeConflict(c *check.C) { 413 d := s.daemon(c) 414 415 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"}) 416 s.mockSnap(c, consumerYaml) 417 s.mockSnap(c, producerYaml) 418 // there is no producer, no slot defined 419 420 s.simulateConflict("consumer") 421 422 action := &client.InterfaceAction{ 423 Action: "connect", 424 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 425 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 426 } 427 text, err := json.Marshal(action) 428 c.Assert(err, check.IsNil) 429 buf := bytes.NewBuffer(text) 430 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 431 c.Assert(err, check.IsNil) 432 rec := httptest.NewRecorder() 433 s.req(c, req, nil).ServeHTTP(rec, req) 434 c.Check(rec.Code, check.Equals, 409) 435 436 var body map[string]interface{} 437 err = json.Unmarshal(rec.Body.Bytes(), &body) 438 c.Check(err, check.IsNil) 439 c.Check(body, check.DeepEquals, map[string]interface{}{ 440 "status-code": 409., 441 "status": "Conflict", 442 "result": map[string]interface{}{ 443 "message": `snap "consumer" has "manip" change in progress`, 444 "kind": "snap-change-conflict", 445 "value": map[string]interface{}{ 446 "change-kind": "manip", 447 "snap-name": "consumer", 448 }, 449 }, 450 "type": "error"}) 451 } 452 453 func (s *interfacesSuite) TestConnectCoreSystemAlias(c *check.C) { 454 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 455 defer revert() 456 d := s.daemon(c) 457 458 s.mockSnap(c, consumerYaml) 459 s.mockSnap(c, coreProducerYaml) 460 461 d.Overlord().Loop() 462 defer d.Overlord().Stop() 463 464 action := &client.InterfaceAction{ 465 Action: "connect", 466 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 467 Slots: []client.Slot{{Snap: "system", Name: "slot"}}, 468 } 469 text, err := json.Marshal(action) 470 c.Assert(err, check.IsNil) 471 buf := bytes.NewBuffer(text) 472 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 473 c.Assert(err, check.IsNil) 474 rec := httptest.NewRecorder() 475 s.req(c, req, nil).ServeHTTP(rec, req) 476 c.Check(rec.Code, check.Equals, 202) 477 var body map[string]interface{} 478 err = json.Unmarshal(rec.Body.Bytes(), &body) 479 c.Check(err, check.IsNil) 480 id := body["change"].(string) 481 482 st := d.Overlord().State() 483 st.Lock() 484 chg := st.Change(id) 485 st.Unlock() 486 c.Assert(chg, check.NotNil) 487 488 <-chg.Ready() 489 490 st.Lock() 491 err = chg.Err() 492 st.Unlock() 493 c.Assert(err, check.IsNil) 494 495 repo := d.Overlord().InterfaceManager().Repository() 496 ifaces := repo.Interfaces() 497 c.Assert(ifaces.Connections, check.HasLen, 1) 498 c.Check(ifaces.Connections, check.DeepEquals, []*interfaces.ConnRef{{ 499 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 500 SlotRef: interfaces.SlotRef{Snap: "core", Name: "slot"}}}) 501 } 502 503 func (s *interfacesSuite) testDisconnect(c *check.C, plugSnap, plugName, slotSnap, slotName string) { 504 restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 505 defer restore() 506 // Install an inverse case mapper to exercise the interface mapping at the same time. 507 restore = ifacestate.MockSnapMapper(&inverseCaseMapper{}) 508 defer restore() 509 d := s.daemon(c) 510 511 s.mockSnap(c, consumerYaml) 512 s.mockSnap(c, producerYaml) 513 514 repo := d.Overlord().InterfaceManager().Repository() 515 connRef := &interfaces.ConnRef{ 516 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 517 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 518 } 519 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 520 c.Assert(err, check.IsNil) 521 522 st := d.Overlord().State() 523 st.Lock() 524 st.Set("conns", map[string]interface{}{ 525 "consumer:plug producer:slot": map[string]interface{}{ 526 "interface": "test", 527 }, 528 }) 529 st.Unlock() 530 531 d.Overlord().Loop() 532 defer d.Overlord().Stop() 533 534 action := &client.InterfaceAction{ 535 Action: "disconnect", 536 Plugs: []client.Plug{{Snap: plugSnap, Name: plugName}}, 537 Slots: []client.Slot{{Snap: slotSnap, Name: slotName}}, 538 } 539 text, err := json.Marshal(action) 540 c.Assert(err, check.IsNil) 541 buf := bytes.NewBuffer(text) 542 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 543 c.Assert(err, check.IsNil) 544 rec := httptest.NewRecorder() 545 s.req(c, req, nil).ServeHTTP(rec, req) 546 c.Check(rec.Code, check.Equals, 202) 547 var body map[string]interface{} 548 err = json.Unmarshal(rec.Body.Bytes(), &body) 549 c.Check(err, check.IsNil) 550 id := body["change"].(string) 551 552 st.Lock() 553 chg := st.Change(id) 554 st.Unlock() 555 c.Assert(chg, check.NotNil) 556 557 <-chg.Ready() 558 559 st.Lock() 560 err = chg.Err() 561 st.Unlock() 562 c.Assert(err, check.IsNil) 563 564 ifaces := repo.Interfaces() 565 c.Assert(ifaces.Connections, check.HasLen, 0) 566 } 567 568 func (s *interfacesSuite) TestDisconnectPlugSuccess(c *check.C) { 569 s.testDisconnect(c, "CONSUMER", "plug", "PRODUCER", "slot") 570 } 571 572 func (s *interfacesSuite) TestDisconnectPlugSuccessWithEmptyPlug(c *check.C) { 573 s.testDisconnect(c, "", "", "PRODUCER", "slot") 574 } 575 576 func (s *interfacesSuite) TestDisconnectPlugSuccessWithEmptySlot(c *check.C) { 577 s.testDisconnect(c, "CONSUMER", "plug", "", "") 578 } 579 580 func (s *interfacesSuite) TestDisconnectPlugFailureNoSuchPlug(c *check.C) { 581 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 582 defer revert() 583 s.daemon(c) 584 585 s.mockSnap(c, consumerYaml) 586 s.mockSnap(c, producerYaml) 587 588 action := &client.InterfaceAction{ 589 Action: "disconnect", 590 Plugs: []client.Plug{{Snap: "consumer", Name: "missingplug"}}, 591 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 592 } 593 text, err := json.Marshal(action) 594 c.Assert(err, check.IsNil) 595 buf := bytes.NewBuffer(text) 596 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 597 c.Assert(err, check.IsNil) 598 rec := httptest.NewRecorder() 599 s.req(c, req, nil).ServeHTTP(rec, req) 600 c.Check(rec.Code, check.Equals, 400) 601 var body map[string]interface{} 602 err = json.Unmarshal(rec.Body.Bytes(), &body) 603 c.Check(err, check.IsNil) 604 c.Check(body, check.DeepEquals, map[string]interface{}{ 605 "result": map[string]interface{}{ 606 "message": "snap \"consumer\" has no plug named \"missingplug\"", 607 }, 608 "status": "Bad Request", 609 "status-code": 400.0, 610 "type": "error", 611 }) 612 } 613 614 func (s *interfacesSuite) testDisconnectFailureNoSnap(c *check.C, installedSnap string) { 615 // sanity, either consumer or producer needs to be enabled 616 consumer := installedSnap == "consumer" 617 producer := installedSnap == "producer" 618 c.Assert(consumer || producer, check.Equals, true, check.Commentf("installed snap must be consumer or producer")) 619 620 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 621 defer revert() 622 s.daemon(c) 623 624 if consumer { 625 s.mockSnap(c, consumerYaml) 626 } 627 if producer { 628 s.mockSnap(c, producerYaml) 629 } 630 631 action := &client.InterfaceAction{ 632 Action: "disconnect", 633 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 634 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 635 } 636 text, err := json.Marshal(action) 637 c.Assert(err, check.IsNil) 638 buf := bytes.NewBuffer(text) 639 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 640 c.Assert(err, check.IsNil) 641 rec := httptest.NewRecorder() 642 s.req(c, req, nil).ServeHTTP(rec, req) 643 c.Check(rec.Code, check.Equals, 400) 644 var body map[string]interface{} 645 err = json.Unmarshal(rec.Body.Bytes(), &body) 646 c.Check(err, check.IsNil) 647 648 if producer { 649 c.Check(body, check.DeepEquals, map[string]interface{}{ 650 "result": map[string]interface{}{ 651 "message": "snap \"consumer\" is not installed", 652 }, 653 "status": "Bad Request", 654 "status-code": 400.0, 655 "type": "error", 656 }) 657 } else { 658 c.Check(body, check.DeepEquals, map[string]interface{}{ 659 "result": map[string]interface{}{ 660 "message": "snap \"producer\" is not installed", 661 }, 662 "status": "Bad Request", 663 "status-code": 400.0, 664 "type": "error", 665 }) 666 } 667 } 668 669 func (s *interfacesSuite) TestDisconnectPlugFailureNoPlugSnap(c *check.C) { 670 s.testDisconnectFailureNoSnap(c, "producer") 671 } 672 673 func (s *interfacesSuite) TestDisconnectPlugFailureNoSlotSnap(c *check.C) { 674 s.testDisconnectFailureNoSnap(c, "consumer") 675 } 676 677 func (s *interfacesSuite) TestDisconnectPlugNothingToDo(c *check.C) { 678 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 679 defer revert() 680 s.daemon(c) 681 682 s.mockSnap(c, consumerYaml) 683 s.mockSnap(c, producerYaml) 684 685 action := &client.InterfaceAction{ 686 Action: "disconnect", 687 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 688 Slots: []client.Slot{{Snap: "", Name: ""}}, 689 } 690 text, err := json.Marshal(action) 691 c.Assert(err, check.IsNil) 692 buf := bytes.NewBuffer(text) 693 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 694 c.Assert(err, check.IsNil) 695 rec := httptest.NewRecorder() 696 s.req(c, req, nil).ServeHTTP(rec, req) 697 c.Check(rec.Code, check.Equals, 400) 698 var body map[string]interface{} 699 err = json.Unmarshal(rec.Body.Bytes(), &body) 700 c.Check(err, check.IsNil) 701 c.Check(body, check.DeepEquals, map[string]interface{}{ 702 "result": map[string]interface{}{ 703 "message": "nothing to do", 704 "kind": "interfaces-unchanged", 705 }, 706 "status": "Bad Request", 707 "status-code": 400.0, 708 "type": "error", 709 }) 710 } 711 712 func (s *interfacesSuite) TestDisconnectPlugFailureNoSuchSlot(c *check.C) { 713 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 714 defer revert() 715 s.daemon(c) 716 717 s.mockSnap(c, consumerYaml) 718 s.mockSnap(c, producerYaml) 719 720 action := &client.InterfaceAction{ 721 Action: "disconnect", 722 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 723 Slots: []client.Slot{{Snap: "producer", Name: "missingslot"}}, 724 } 725 text, err := json.Marshal(action) 726 c.Assert(err, check.IsNil) 727 buf := bytes.NewBuffer(text) 728 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 729 c.Assert(err, check.IsNil) 730 rec := httptest.NewRecorder() 731 s.req(c, req, nil).ServeHTTP(rec, req) 732 733 c.Check(rec.Code, check.Equals, 400) 734 var body map[string]interface{} 735 err = json.Unmarshal(rec.Body.Bytes(), &body) 736 c.Check(err, check.IsNil) 737 c.Check(body, check.DeepEquals, map[string]interface{}{ 738 "result": map[string]interface{}{ 739 "message": "snap \"producer\" has no slot named \"missingslot\"", 740 }, 741 "status": "Bad Request", 742 "status-code": 400.0, 743 "type": "error", 744 }) 745 } 746 747 func (s *interfacesSuite) TestDisconnectPlugFailureNotConnected(c *check.C) { 748 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 749 defer revert() 750 s.daemon(c) 751 752 s.mockSnap(c, consumerYaml) 753 s.mockSnap(c, producerYaml) 754 755 action := &client.InterfaceAction{ 756 Action: "disconnect", 757 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 758 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 759 } 760 text, err := json.Marshal(action) 761 c.Assert(err, check.IsNil) 762 buf := bytes.NewBuffer(text) 763 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 764 c.Assert(err, check.IsNil) 765 rec := httptest.NewRecorder() 766 s.req(c, req, nil).ServeHTTP(rec, req) 767 768 c.Check(rec.Code, check.Equals, 400) 769 var body map[string]interface{} 770 err = json.Unmarshal(rec.Body.Bytes(), &body) 771 c.Check(err, check.IsNil) 772 c.Check(body, check.DeepEquals, map[string]interface{}{ 773 "result": map[string]interface{}{ 774 "message": "cannot disconnect consumer:plug from producer:slot, it is not connected", 775 }, 776 "status": "Bad Request", 777 "status-code": 400.0, 778 "type": "error", 779 }) 780 } 781 782 func (s *interfacesSuite) TestDisconnectForgetPlugFailureNotConnected(c *check.C) { 783 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 784 defer revert() 785 s.daemon(c) 786 787 s.mockSnap(c, consumerYaml) 788 s.mockSnap(c, producerYaml) 789 790 action := &client.InterfaceAction{ 791 Action: "disconnect", 792 Forget: true, 793 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 794 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 795 } 796 text, err := json.Marshal(action) 797 c.Assert(err, check.IsNil) 798 buf := bytes.NewBuffer(text) 799 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 800 c.Assert(err, check.IsNil) 801 rec := httptest.NewRecorder() 802 s.req(c, req, nil).ServeHTTP(rec, req) 803 804 c.Check(rec.Code, check.Equals, 400) 805 var body map[string]interface{} 806 err = json.Unmarshal(rec.Body.Bytes(), &body) 807 c.Check(err, check.IsNil) 808 c.Check(body, check.DeepEquals, map[string]interface{}{ 809 "result": map[string]interface{}{ 810 "message": "cannot forget connection consumer:plug from producer:slot, it was not connected", 811 }, 812 "status": "Bad Request", 813 "status-code": 400.0, 814 "type": "error", 815 }) 816 } 817 818 func (s *interfacesSuite) TestDisconnectConflict(c *check.C) { 819 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 820 defer revert() 821 d := s.daemon(c) 822 823 s.mockSnap(c, consumerYaml) 824 s.mockSnap(c, producerYaml) 825 826 repo := d.Overlord().InterfaceManager().Repository() 827 connRef := &interfaces.ConnRef{ 828 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 829 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 830 } 831 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 832 c.Assert(err, check.IsNil) 833 834 st := d.Overlord().State() 835 st.Lock() 836 st.Set("conns", map[string]interface{}{ 837 "consumer:plug producer:slot": map[string]interface{}{ 838 "interface": "test", 839 }, 840 }) 841 st.Unlock() 842 843 s.simulateConflict("consumer") 844 845 action := &client.InterfaceAction{ 846 Action: "disconnect", 847 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 848 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 849 } 850 text, err := json.Marshal(action) 851 c.Assert(err, check.IsNil) 852 buf := bytes.NewBuffer(text) 853 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 854 c.Assert(err, check.IsNil) 855 rec := httptest.NewRecorder() 856 s.req(c, req, nil).ServeHTTP(rec, req) 857 858 c.Check(rec.Code, check.Equals, 409) 859 860 var body map[string]interface{} 861 err = json.Unmarshal(rec.Body.Bytes(), &body) 862 c.Check(err, check.IsNil) 863 c.Check(body, check.DeepEquals, map[string]interface{}{ 864 "status-code": 409., 865 "status": "Conflict", 866 "result": map[string]interface{}{ 867 "message": `snap "consumer" has "manip" change in progress`, 868 "kind": "snap-change-conflict", 869 "value": map[string]interface{}{ 870 "change-kind": "manip", 871 "snap-name": "consumer", 872 }, 873 }, 874 "type": "error"}) 875 } 876 877 func (s *interfacesSuite) TestDisconnectCoreSystemAlias(c *check.C) { 878 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 879 defer revert() 880 d := s.daemon(c) 881 882 s.mockSnap(c, consumerYaml) 883 s.mockSnap(c, coreProducerYaml) 884 885 repo := d.Overlord().InterfaceManager().Repository() 886 connRef := &interfaces.ConnRef{ 887 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 888 SlotRef: interfaces.SlotRef{Snap: "core", Name: "slot"}, 889 } 890 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 891 c.Assert(err, check.IsNil) 892 893 st := d.Overlord().State() 894 st.Lock() 895 st.Set("conns", map[string]interface{}{ 896 "consumer:plug core:slot": map[string]interface{}{ 897 "interface": "test", 898 }, 899 }) 900 st.Unlock() 901 902 d.Overlord().Loop() 903 defer d.Overlord().Stop() 904 905 action := &client.InterfaceAction{ 906 Action: "disconnect", 907 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 908 Slots: []client.Slot{{Snap: "system", Name: "slot"}}, 909 } 910 text, err := json.Marshal(action) 911 c.Assert(err, check.IsNil) 912 buf := bytes.NewBuffer(text) 913 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 914 c.Assert(err, check.IsNil) 915 rec := httptest.NewRecorder() 916 s.req(c, req, nil).ServeHTTP(rec, req) 917 c.Check(rec.Code, check.Equals, 202) 918 var body map[string]interface{} 919 err = json.Unmarshal(rec.Body.Bytes(), &body) 920 c.Check(err, check.IsNil) 921 id := body["change"].(string) 922 923 st.Lock() 924 chg := st.Change(id) 925 st.Unlock() 926 c.Assert(chg, check.NotNil) 927 928 <-chg.Ready() 929 930 st.Lock() 931 err = chg.Err() 932 st.Unlock() 933 c.Assert(err, check.IsNil) 934 935 ifaces := repo.Interfaces() 936 c.Assert(ifaces.Connections, check.HasLen, 0) 937 } 938 939 func (s *interfacesSuite) TestUnsupportedInterfaceRequest(c *check.C) { 940 s.daemon(c) 941 buf := bytes.NewBuffer([]byte(`garbage`)) 942 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 943 c.Assert(err, check.IsNil) 944 rec := httptest.NewRecorder() 945 s.req(c, req, nil).ServeHTTP(rec, req) 946 c.Check(rec.Code, check.Equals, 400) 947 var body map[string]interface{} 948 err = json.Unmarshal(rec.Body.Bytes(), &body) 949 c.Check(err, check.IsNil) 950 c.Check(body, check.DeepEquals, map[string]interface{}{ 951 "result": map[string]interface{}{ 952 "message": "cannot decode request body into an interface action: invalid character 'g' looking for beginning of value", 953 }, 954 "status": "Bad Request", 955 "status-code": 400.0, 956 "type": "error", 957 }) 958 } 959 960 func (s *interfacesSuite) TestMissingInterfaceAction(c *check.C) { 961 s.daemon(c) 962 action := &client.InterfaceAction{} 963 text, err := json.Marshal(action) 964 c.Assert(err, check.IsNil) 965 buf := bytes.NewBuffer(text) 966 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 967 c.Assert(err, check.IsNil) 968 rec := httptest.NewRecorder() 969 s.req(c, req, nil).ServeHTTP(rec, req) 970 c.Check(rec.Code, check.Equals, 400) 971 var body map[string]interface{} 972 err = json.Unmarshal(rec.Body.Bytes(), &body) 973 c.Check(err, check.IsNil) 974 c.Check(body, check.DeepEquals, map[string]interface{}{ 975 "result": map[string]interface{}{ 976 "message": "interface action not specified", 977 }, 978 "status": "Bad Request", 979 "status-code": 400.0, 980 "type": "error", 981 }) 982 } 983 984 func (s *interfacesSuite) TestUnsupportedInterfaceAction(c *check.C) { 985 s.daemon(c) 986 action := &client.InterfaceAction{Action: "foo"} 987 text, err := json.Marshal(action) 988 c.Assert(err, check.IsNil) 989 buf := bytes.NewBuffer(text) 990 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 991 c.Assert(err, check.IsNil) 992 rec := httptest.NewRecorder() 993 s.req(c, req, nil).ServeHTTP(rec, req) 994 c.Check(rec.Code, check.Equals, 400) 995 var body map[string]interface{} 996 err = json.Unmarshal(rec.Body.Bytes(), &body) 997 c.Check(err, check.IsNil) 998 c.Check(body, check.DeepEquals, map[string]interface{}{ 999 "result": map[string]interface{}{ 1000 "message": "unsupported interface action: \"foo\"", 1001 }, 1002 "status": "Bad Request", 1003 "status-code": 400.0, 1004 "type": "error", 1005 }) 1006 } 1007 1008 // Tests for GET /v2/interfaces 1009 1010 func (s *interfacesSuite) TestInterfacesLegacy(c *check.C) { 1011 restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 1012 defer restore() 1013 // Install an inverse case mapper to exercise the interface mapping at the same time. 1014 restore = ifacestate.MockSnapMapper(&inverseCaseMapper{}) 1015 defer restore() 1016 1017 d := s.daemon(c) 1018 1019 var anotherConsumerYaml = ` 1020 name: another-consumer-%s 1021 version: 1 1022 apps: 1023 app: 1024 plugs: 1025 plug: 1026 interface: test 1027 key: value 1028 label: label 1029 ` 1030 s.mockSnap(c, consumerYaml) 1031 s.mockSnap(c, fmt.Sprintf(anotherConsumerYaml, "def")) 1032 s.mockSnap(c, fmt.Sprintf(anotherConsumerYaml, "abc")) 1033 s.mockSnap(c, producerYaml) 1034 1035 repo := d.Overlord().InterfaceManager().Repository() 1036 connRef := &interfaces.ConnRef{ 1037 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 1038 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 1039 } 1040 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 1041 c.Assert(err, check.IsNil) 1042 1043 st := d.Overlord().State() 1044 st.Lock() 1045 st.Set("conns", map[string]interface{}{ 1046 "consumer:plug producer:slot": map[string]interface{}{ 1047 "interface": "test", 1048 "auto": true, 1049 }, 1050 "another-consumer-def:plug producer:slot": map[string]interface{}{ 1051 "interface": "test", 1052 "by-gadget": true, 1053 "auto": true, 1054 }, 1055 "another-consumer-abc:plug producer:slot": map[string]interface{}{ 1056 "interface": "test", 1057 "by-gadget": true, 1058 "auto": true, 1059 }, 1060 }) 1061 st.Unlock() 1062 1063 req, err := http.NewRequest("GET", "/v2/interfaces", nil) 1064 c.Assert(err, check.IsNil) 1065 rec := httptest.NewRecorder() 1066 s.req(c, req, nil).ServeHTTP(rec, req) 1067 c.Check(rec.Code, check.Equals, 200) 1068 var body map[string]interface{} 1069 err = json.Unmarshal(rec.Body.Bytes(), &body) 1070 c.Check(err, check.IsNil) 1071 c.Check(body, check.DeepEquals, map[string]interface{}{ 1072 "result": map[string]interface{}{ 1073 "plugs": []interface{}{ 1074 map[string]interface{}{ 1075 "snap": "another-consumer-abc", 1076 "plug": "plug", 1077 "interface": "test", 1078 "attrs": map[string]interface{}{"key": "value"}, 1079 "apps": []interface{}{"app"}, 1080 "label": "label", 1081 "connections": []interface{}{ 1082 map[string]interface{}{"snap": "producer", "slot": "slot"}, 1083 }, 1084 }, 1085 map[string]interface{}{ 1086 "snap": "another-consumer-def", 1087 "plug": "plug", 1088 "interface": "test", 1089 "attrs": map[string]interface{}{"key": "value"}, 1090 "apps": []interface{}{"app"}, 1091 "label": "label", 1092 "connections": []interface{}{ 1093 map[string]interface{}{"snap": "producer", "slot": "slot"}, 1094 }, 1095 }, 1096 map[string]interface{}{ 1097 "snap": "consumer", 1098 "plug": "plug", 1099 "interface": "test", 1100 "attrs": map[string]interface{}{"key": "value"}, 1101 "apps": []interface{}{"app"}, 1102 "label": "label", 1103 "connections": []interface{}{ 1104 map[string]interface{}{"snap": "producer", "slot": "slot"}, 1105 }, 1106 }, 1107 }, 1108 "slots": []interface{}{ 1109 map[string]interface{}{ 1110 "snap": "producer", 1111 "slot": "slot", 1112 "interface": "test", 1113 "attrs": map[string]interface{}{"key": "value"}, 1114 "apps": []interface{}{"app"}, 1115 "label": "label", 1116 "connections": []interface{}{ 1117 map[string]interface{}{"snap": "another-consumer-abc", "plug": "plug"}, 1118 map[string]interface{}{"snap": "another-consumer-def", "plug": "plug"}, 1119 map[string]interface{}{"snap": "consumer", "plug": "plug"}, 1120 }, 1121 }, 1122 }, 1123 }, 1124 "status": "OK", 1125 "status-code": 200.0, 1126 "type": "sync", 1127 }) 1128 } 1129 1130 func (s *interfacesSuite) TestInterfacesModern(c *check.C) { 1131 restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 1132 defer restore() 1133 // Install an inverse case mapper to exercise the interface mapping at the same time. 1134 restore = ifacestate.MockSnapMapper(&inverseCaseMapper{}) 1135 defer restore() 1136 1137 d := s.daemon(c) 1138 1139 s.mockSnap(c, consumerYaml) 1140 s.mockSnap(c, producerYaml) 1141 1142 repo := d.Overlord().InterfaceManager().Repository() 1143 connRef := &interfaces.ConnRef{ 1144 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 1145 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 1146 } 1147 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 1148 c.Assert(err, check.IsNil) 1149 1150 req, err := http.NewRequest("GET", "/v2/interfaces?select=connected&doc=true&plugs=true&slots=true", nil) 1151 c.Assert(err, check.IsNil) 1152 rec := httptest.NewRecorder() 1153 s.req(c, req, nil).ServeHTTP(rec, req) 1154 c.Check(rec.Code, check.Equals, 200) 1155 var body map[string]interface{} 1156 err = json.Unmarshal(rec.Body.Bytes(), &body) 1157 c.Check(err, check.IsNil) 1158 c.Check(body, check.DeepEquals, map[string]interface{}{ 1159 "result": []interface{}{ 1160 map[string]interface{}{ 1161 "name": "test", 1162 "plugs": []interface{}{ 1163 map[string]interface{}{ 1164 "snap": "consumer", 1165 "plug": "plug", 1166 "label": "label", 1167 "attrs": map[string]interface{}{ 1168 "key": "value", 1169 }, 1170 }}, 1171 "slots": []interface{}{ 1172 map[string]interface{}{ 1173 "snap": "producer", 1174 "slot": "slot", 1175 "label": "label", 1176 "attrs": map[string]interface{}{ 1177 "key": "value", 1178 }, 1179 }, 1180 }, 1181 }, 1182 }, 1183 "status": "OK", 1184 "status-code": 200.0, 1185 "type": "sync", 1186 }) 1187 }